imprimatur 0.2.0 → 0.4.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 +153 -3
- data/Cargo.toml +1 -1
- data/README.md +139 -21
- data/ext/imprimatur_native/cli/Cargo.toml +3 -1
- data/ext/imprimatur_native/core/Cargo.toml +20 -4
- data/ext/imprimatur_native/core/README.md +36 -4
- data/ext/imprimatur_native/core/src/archive.rs +3 -16
- data/ext/imprimatur_native/core/src/barcode/code128.rs +162 -0
- data/ext/imprimatur_native/core/src/barcode/draw.rs +124 -0
- data/ext/imprimatur_native/core/src/barcode/ean.rs +91 -0
- data/ext/imprimatur_native/core/src/barcode/gs1.rs +188 -0
- data/ext/imprimatur_native/core/src/barcode/matrix.rs +130 -0
- data/ext/imprimatur_native/core/src/barcode.rs +314 -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/symbol.rs +136 -0
- data/ext/imprimatur_native/core/src/doc/table.rs +21 -0
- data/ext/imprimatur_native/core/src/doc.rs +96 -2
- data/ext/imprimatur_native/core/src/docx/picture.rs +72 -0
- data/ext/imprimatur_native/core/src/docx.rs +103 -28
- data/ext/imprimatur_native/core/src/error.rs +18 -0
- data/ext/imprimatur_native/core/src/font.rs +231 -0
- data/ext/imprimatur_native/core/src/lib.rs +6 -1
- data/ext/imprimatur_native/core/src/pdf/layout.rs +72 -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/truetype/descriptor.rs +129 -0
- data/ext/imprimatur_native/core/src/pdf/truetype/program.rs +70 -0
- data/ext/imprimatur_native/core/src/pdf/truetype.rs +149 -0
- data/ext/imprimatur_native/core/src/pdf.rs +40 -4
- data/ext/imprimatur_native/core/src/read/content.rs +75 -2
- data/ext/imprimatur_native/core/src/read/image.rs +17 -0
- 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 +5 -1
- data/ext/imprimatur_native/core/src/script.rs +116 -0
- data/ext/imprimatur_native/core/src/style.rs +32 -0
- 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/barcode.rs +147 -0
- data/ext/imprimatur_native/ffi/src/document.rs +76 -1
- data/ext/imprimatur_native/ffi/src/lib.rs +40 -1
- data/ext/imprimatur_native/ffi/src/read.rs +8 -11
- data/ext/imprimatur_native/ffi/src/sheets.rs +114 -0
- data/lib/imprimatur/barcode.rb +78 -0
- data/lib/imprimatur/document.rb +46 -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 +19 -3
- metadata +23 -2
- data/ext/imprimatur_native/core/src/test_support.rs +0 -250
|
@@ -0,0 +1,231 @@
|
|
|
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
|
+
/// `Malformed`, categorized as TrueType font data: parsing, embedding and PDF
|
|
228
|
+
/// resource errors ([`crate::pdf::truetype`]) all point at the same category.
|
|
229
|
+
pub(crate) fn invalid(reason: impl std::fmt::Display) -> Error {
|
|
230
|
+
Error::malformed("TrueType font", reason.to_string())
|
|
231
|
+
}
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
//! (deflate) and `crc32fast`.
|
|
4
4
|
//!
|
|
5
5
|
//! [`xlsx`] writes workbooks; [`doc`] is one document model that renders to
|
|
6
|
-
//! PDF and DOCX
|
|
6
|
+
//! PDF and DOCX; [`barcode`] draws QR Code, Data Matrix, EAN, UPC and Code
|
|
7
|
+
//! 128 symbols, GS1 ones included.
|
|
7
8
|
//!
|
|
8
9
|
//! ```
|
|
9
10
|
//! use imprimatur::xlsx::{Cell, Workbook};
|
|
@@ -20,12 +21,16 @@
|
|
|
20
21
|
|
|
21
22
|
#[cfg(feature = "archive")]
|
|
22
23
|
pub mod archive;
|
|
24
|
+
pub mod barcode;
|
|
23
25
|
pub mod doc;
|
|
24
26
|
mod docx;
|
|
25
27
|
mod error;
|
|
28
|
+
#[cfg(feature = "fonts")]
|
|
29
|
+
pub mod font;
|
|
26
30
|
mod opc;
|
|
27
31
|
mod pdf;
|
|
28
32
|
pub mod read;
|
|
33
|
+
mod script;
|
|
29
34
|
pub mod style;
|
|
30
35
|
#[cfg(test)]
|
|
31
36
|
mod test_support;
|
|
@@ -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, Symbol, 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,35 @@ 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 (width, height) = picture.size(self.room());
|
|
133
|
+
let (x, y) = self.place((width, height), picture.align);
|
|
134
|
+
let (width, height, x, y) = (Num(width), Num(height), Num(x), Num(y));
|
|
135
|
+
emit(self.current(), format_args!("q {width} 0 0 {height} {x} {y} cm /Im{index} Do Q\n"));
|
|
136
|
+
self.end_figure();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/// Draws a barcode as filled rectangles, one module a unit, the
|
|
140
|
+
/// coordinates flipped to run down from its top left corner.
|
|
141
|
+
pub fn symbol(&mut self, symbol: &Symbol) {
|
|
142
|
+
let (shapes, (width, height)) = symbol.placed(self.room());
|
|
143
|
+
let (x, y) = self.place((width, height), symbol.align);
|
|
144
|
+
let (columns, rows) = shapes.extent();
|
|
145
|
+
let (across, down) = (width / columns, height / rows);
|
|
146
|
+
let (fill, top, x) = (Fill(symbol.color), Num(y + height), Num(x));
|
|
147
|
+
let page = self.current();
|
|
148
|
+
// Four decimals: a module's size in hundredths of a point would be
|
|
149
|
+
// off by up to half a percent across the symbol.
|
|
150
|
+
emit(page, format_args!("q {fill} {across:.4} 0 0 {:.4} {x} {top} cm\n", -down));
|
|
151
|
+
for [x, y, run, rows] in &shapes.rects {
|
|
152
|
+
emit(page, format_args!("{x} {y} {run} {rows} re\n"));
|
|
153
|
+
}
|
|
154
|
+
page.extend_from_slice(b"f Q\n");
|
|
155
|
+
self.end_figure();
|
|
156
|
+
}
|
|
157
|
+
|
|
124
158
|
pub fn page_break(&mut self) {
|
|
125
159
|
self.new_page();
|
|
126
160
|
}
|
|
@@ -129,24 +163,48 @@ impl Layout {
|
|
|
129
163
|
pub fn finish(mut self, page_numbers: bool) -> Vec<Vec<u8>> {
|
|
130
164
|
if page_numbers {
|
|
131
165
|
let total = self.pages.len();
|
|
132
|
-
let center = self.width / 2.0;
|
|
166
|
+
let (center, margin) = (self.width / 2.0, self.margin);
|
|
133
167
|
for (index, page) in self.pages.iter_mut().enumerate() {
|
|
134
168
|
let bytes = format!("{} / {total}", index + 1).into_bytes();
|
|
135
169
|
let width = Face::Regular.width(&bytes, FOOTER_SIZE);
|
|
136
170
|
let look =
|
|
137
171
|
Look { face: Face::Regular, color: Some(FOOTER_COLOR), underline: false };
|
|
138
172
|
let label = wrap::Fragment { bytes, look, width };
|
|
139
|
-
draw_fragment(page, &label, FOOTER_SIZE, center - width / 2.0,
|
|
173
|
+
draw_fragment(page, &label, FOOTER_SIZE, center - width / 2.0, margin / 2.0);
|
|
140
174
|
}
|
|
141
175
|
}
|
|
142
176
|
self.pages
|
|
143
177
|
}
|
|
144
178
|
|
|
179
|
+
/// The width and height between the margins.
|
|
180
|
+
const fn room(&self) -> (f64, f64) {
|
|
181
|
+
(self.content_width(), (-2.0f64).mul_add(self.margin, self.height))
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/// Makes room for a figure of `size`, on a new page when it does not
|
|
185
|
+
/// fit, and returns its bottom left corner as `align` places it.
|
|
186
|
+
fn place(&mut self, (width, height): (f64, f64), align: Option<Align>) -> (f64, f64) {
|
|
187
|
+
self.ensure_room(height);
|
|
188
|
+
let x = self.margin
|
|
189
|
+
+ match align.unwrap_or(Align::Left) {
|
|
190
|
+
Align::Left => 0.0,
|
|
191
|
+
Align::Center => (self.content_width() - width) / 2.0,
|
|
192
|
+
Align::Right => self.content_width() - width,
|
|
193
|
+
};
|
|
194
|
+
self.y -= height;
|
|
195
|
+
(x, self.y)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
fn end_figure(&mut self) {
|
|
199
|
+
self.y -= PARAGRAPH_GAP;
|
|
200
|
+
self.blank = false;
|
|
201
|
+
}
|
|
202
|
+
|
|
145
203
|
/// Starts a new page unless `needed` points fit above the bottom margin
|
|
146
204
|
/// (or the page is still blank, where nothing would fit better).
|
|
147
205
|
/// Returns whether it did.
|
|
148
206
|
fn ensure_room(&mut self, needed: f64) -> bool {
|
|
149
|
-
let full = !self.blank && self.y - needed <
|
|
207
|
+
let full = !self.blank && self.y - needed < self.margin;
|
|
150
208
|
if full {
|
|
151
209
|
self.new_page();
|
|
152
210
|
}
|
|
@@ -155,7 +213,7 @@ impl Layout {
|
|
|
155
213
|
|
|
156
214
|
fn new_page(&mut self) {
|
|
157
215
|
self.pages.push(Vec::new());
|
|
158
|
-
self.y = self.height -
|
|
216
|
+
self.y = self.height - self.margin;
|
|
159
217
|
self.blank = true;
|
|
160
218
|
}
|
|
161
219
|
|
|
@@ -174,7 +232,7 @@ impl Layout {
|
|
|
174
232
|
self.ensure_room(leading);
|
|
175
233
|
let baseline = self.y - baseline_offset(size);
|
|
176
234
|
let slack = self.content_width() - line.width;
|
|
177
|
-
let x =
|
|
235
|
+
let x = self.margin
|
|
178
236
|
+ match align {
|
|
179
237
|
Align::Left => 0.0,
|
|
180
238
|
Align::Center => slack / 2.0,
|
|
@@ -188,9 +246,8 @@ impl Layout {
|
|
|
188
246
|
|
|
189
247
|
/// One table row from the cursor down: fill, borders, then text.
|
|
190
248
|
fn row(&mut self, cells: &[Vec<Line>], height: f64, column_width: f64, header: bool) {
|
|
191
|
-
let top = self.y;
|
|
249
|
+
let (top, mut x) = (self.y, self.margin);
|
|
192
250
|
let page = self.current();
|
|
193
|
-
let mut x = MARGIN;
|
|
194
251
|
for lines in cells {
|
|
195
252
|
let rect = format_args!(
|
|
196
253
|
"{} {} {} {} 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
|
+
}
|