imprimatur 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +8 -0
- data/Cargo.lock +835 -0
- data/Cargo.toml +44 -0
- data/LICENSE.txt +29 -0
- data/README.md +314 -0
- data/ext/imprimatur_native/cli/Cargo.toml +30 -0
- data/ext/imprimatur_native/cli/src/app.rs +183 -0
- data/ext/imprimatur_native/cli/src/cli/completion.rs +26 -0
- data/ext/imprimatur_native/cli/src/cli/grep.rs +359 -0
- data/ext/imprimatur_native/cli/src/cli/images.rs +137 -0
- data/ext/imprimatur_native/cli/src/cli/limits.rs +49 -0
- data/ext/imprimatur_native/cli/src/cli/markdown.rs +128 -0
- data/ext/imprimatur_native/cli/src/cli.rs +71 -0
- data/ext/imprimatur_native/cli/src/main.rs +15 -0
- data/ext/imprimatur_native/core/Cargo.toml +33 -0
- data/ext/imprimatur_native/core/LICENSE +29 -0
- data/ext/imprimatur_native/core/README.md +105 -0
- data/ext/imprimatur_native/core/src/doc/paragraph.rs +112 -0
- data/ext/imprimatur_native/core/src/doc/table.rs +41 -0
- data/ext/imprimatur_native/core/src/doc.rs +210 -0
- data/ext/imprimatur_native/core/src/docx.rs +343 -0
- data/ext/imprimatur_native/core/src/error.rs +113 -0
- data/ext/imprimatur_native/core/src/lib.rs +38 -0
- data/ext/imprimatur_native/core/src/opc.rs +51 -0
- data/ext/imprimatur_native/core/src/pdf/font.rs +96 -0
- data/ext/imprimatur_native/core/src/pdf/layout/wrap.rs +161 -0
- data/ext/imprimatur_native/core/src/pdf/layout.rs +280 -0
- data/ext/imprimatur_native/core/src/pdf/metrics.rs +139 -0
- data/ext/imprimatur_native/core/src/pdf/objects.rs +141 -0
- data/ext/imprimatur_native/core/src/pdf.rs +84 -0
- data/ext/imprimatur_native/core/src/read/budget.rs +140 -0
- data/ext/imprimatur_native/core/src/read/content.rs +266 -0
- data/ext/imprimatur_native/core/src/read/docx/styles.rs +118 -0
- data/ext/imprimatur_native/core/src/read/docx.rs +336 -0
- data/ext/imprimatur_native/core/src/read/image/dimensions.rs +215 -0
- data/ext/imprimatur_native/core/src/read/image.rs +233 -0
- data/ext/imprimatur_native/core/src/read/inflate.rs +44 -0
- data/ext/imprimatur_native/core/src/read/json.rs +172 -0
- data/ext/imprimatur_native/core/src/read/markdown.rs +254 -0
- data/ext/imprimatur_native/core/src/read/package.rs +191 -0
- data/ext/imprimatur_native/core/src/read/pdf/cmap.rs +216 -0
- data/ext/imprimatur_native/core/src/read/pdf/content/geometry.rs +79 -0
- data/ext/imprimatur_native/core/src/read/pdf/content/operands.rs +156 -0
- data/ext/imprimatur_native/core/src/read/pdf/content.rs +439 -0
- data/ext/imprimatur_native/core/src/read/pdf/crypt.rs +256 -0
- data/ext/imprimatur_native/core/src/read/pdf/document.rs +420 -0
- data/ext/imprimatur_native/core/src/read/pdf/filter.rs +258 -0
- data/ext/imprimatur_native/core/src/read/pdf/font.rs +315 -0
- data/ext/imprimatur_native/core/src/read/pdf/glyphs.rs +179 -0
- data/ext/imprimatur_native/core/src/read/pdf/glyphs.txt +6298 -0
- data/ext/imprimatur_native/core/src/read/pdf/images.rs +365 -0
- data/ext/imprimatur_native/core/src/read/pdf/layout.rs +451 -0
- data/ext/imprimatur_native/core/src/read/pdf/lexer.rs +255 -0
- data/ext/imprimatur_native/core/src/read/pdf/object/name.rs +44 -0
- data/ext/imprimatur_native/core/src/read/pdf/object.rs +261 -0
- data/ext/imprimatur_native/core/src/read/pdf/xref.rs +346 -0
- data/ext/imprimatur_native/core/src/read/pdf.rs +53 -0
- data/ext/imprimatur_native/core/src/read/span.rs +22 -0
- data/ext/imprimatur_native/core/src/read/xlsx/numbers.rs +155 -0
- data/ext/imprimatur_native/core/src/read/xlsx.rs +340 -0
- data/ext/imprimatur_native/core/src/read.rs +145 -0
- data/ext/imprimatur_native/core/src/style.rs +82 -0
- data/ext/imprimatur_native/core/src/test_support.rs +163 -0
- data/ext/imprimatur_native/core/src/xlsx/cell.rs +112 -0
- data/ext/imprimatur_native/core/src/xlsx/comment.rs +256 -0
- data/ext/imprimatur_native/core/src/xlsx/date.rs +173 -0
- data/ext/imprimatur_native/core/src/xlsx/format.rs +226 -0
- data/ext/imprimatur_native/core/src/xlsx/package.rs +268 -0
- data/ext/imprimatur_native/core/src/xlsx/range.rs +124 -0
- data/ext/imprimatur_native/core/src/xlsx/styles.rs +240 -0
- data/ext/imprimatur_native/core/src/xlsx/text.rs +122 -0
- data/ext/imprimatur_native/core/src/xlsx/validation.rs +258 -0
- data/ext/imprimatur_native/core/src/xlsx/worksheet.rs +350 -0
- data/ext/imprimatur_native/core/src/xlsx.rs +120 -0
- data/ext/imprimatur_native/core/src/xml/reader.rs +238 -0
- data/ext/imprimatur_native/core/src/xml.rs +95 -0
- data/ext/imprimatur_native/core/src/zip/reader.rs +144 -0
- data/ext/imprimatur_native/core/src/zip.rs +161 -0
- data/ext/imprimatur_native/extconf.rb +45 -0
- data/ext/imprimatur_native/ffi/Cargo.toml +21 -0
- data/ext/imprimatur_native/ffi/src/convert.rs +132 -0
- data/ext/imprimatur_native/ffi/src/document.rs +138 -0
- data/ext/imprimatur_native/ffi/src/lib.rs +301 -0
- data/ext/imprimatur_native/ffi/src/read.rs +182 -0
- data/ext/imprimatur_native/ffi/src/style.rs +85 -0
- data/ext/imprimatur_native/ffi/src/validation.rs +56 -0
- data/lib/imprimatur/document.rb +127 -0
- data/lib/imprimatur/error.rb +22 -0
- data/lib/imprimatur/formula.rb +13 -0
- data/lib/imprimatur/native.rb +17 -0
- data/lib/imprimatur/reader.rb +59 -0
- data/lib/imprimatur/style.rb +31 -0
- data/lib/imprimatur/validation.rb +35 -0
- data/lib/imprimatur/version.rb +5 -0
- data/lib/imprimatur/xlsx.rb +183 -0
- data/lib/imprimatur.rb +111 -0
- metadata +157 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
//! Greedy line breaking across runs of encoded text.
|
|
2
|
+
|
|
3
|
+
#[cfg(test)]
|
|
4
|
+
mod tests;
|
|
5
|
+
|
|
6
|
+
use super::super::font::Face;
|
|
7
|
+
use crate::style::Color;
|
|
8
|
+
|
|
9
|
+
/// How a piece of text is drawn.
|
|
10
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
11
|
+
pub struct Look {
|
|
12
|
+
pub face: Face,
|
|
13
|
+
pub color: Option<Color>,
|
|
14
|
+
pub underline: bool,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/// Text with one look on one line.
|
|
18
|
+
#[derive(Debug, Clone, PartialEq)]
|
|
19
|
+
pub struct Fragment {
|
|
20
|
+
pub bytes: Vec<u8>,
|
|
21
|
+
pub look: Look,
|
|
22
|
+
pub width: f64,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
#[derive(Debug, Clone, Default, PartialEq)]
|
|
26
|
+
pub struct Line {
|
|
27
|
+
pub fragments: Vec<Fragment>,
|
|
28
|
+
pub width: f64,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
impl Line {
|
|
32
|
+
/// Appends text, merging it into the last fragment when the look matches.
|
|
33
|
+
fn push(&mut self, bytes: &[u8], look: Look, width: f64) {
|
|
34
|
+
self.width += width;
|
|
35
|
+
if let Some(last) = self.fragments.last_mut()
|
|
36
|
+
&& last.look == look
|
|
37
|
+
{
|
|
38
|
+
last.bytes.extend_from_slice(bytes);
|
|
39
|
+
last.width += width;
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
self.fragments.push(Fragment { bytes: bytes.to_vec(), look, width });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const fn is_empty(&self) -> bool {
|
|
46
|
+
self.fragments.is_empty()
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// Spaces a tab stands for.
|
|
51
|
+
const TAB_SPACES: &[u8] = b" ";
|
|
52
|
+
|
|
53
|
+
/// Breaks `runs` into lines no wider than `max_width` at `size` points.
|
|
54
|
+
/// Lines break at spaces; `\n` forces a break; a word wider than a line is
|
|
55
|
+
/// split between characters. Spaces where a line wraps are dropped.
|
|
56
|
+
pub fn wrap(runs: &[(Vec<u8>, Look)], size: f64, max_width: f64) -> Vec<Line> {
|
|
57
|
+
let mut lines = vec![Line::default()];
|
|
58
|
+
// Whitespace seen since the last word, placed only if a word follows on
|
|
59
|
+
// the same line.
|
|
60
|
+
let mut pending: Vec<(Vec<u8>, Look)> = Vec::new();
|
|
61
|
+
for (bytes, look) in runs {
|
|
62
|
+
let face = look.face;
|
|
63
|
+
for token in tokens(bytes) {
|
|
64
|
+
match token {
|
|
65
|
+
Token::Newline => {
|
|
66
|
+
lines.push(Line::default());
|
|
67
|
+
pending.clear();
|
|
68
|
+
}
|
|
69
|
+
Token::Space(space) => {
|
|
70
|
+
let mut expanded = Vec::with_capacity(space.len());
|
|
71
|
+
for &byte in space {
|
|
72
|
+
if byte == b'\t' {
|
|
73
|
+
expanded.extend_from_slice(TAB_SPACES);
|
|
74
|
+
} else {
|
|
75
|
+
expanded.push(byte);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
pending.push((expanded, *look));
|
|
79
|
+
}
|
|
80
|
+
Token::Word(word) => {
|
|
81
|
+
let width = face.width(word, size);
|
|
82
|
+
let spaces: f64 =
|
|
83
|
+
pending.iter().map(|(space, look)| look.face.width(space, size)).sum();
|
|
84
|
+
let line = lines.last_mut().map_or(0.0, |line| line.width);
|
|
85
|
+
let started = lines.last().is_some_and(|line| !line.is_empty());
|
|
86
|
+
if started && line + spaces + width > max_width {
|
|
87
|
+
lines.push(Line::default());
|
|
88
|
+
pending.clear();
|
|
89
|
+
}
|
|
90
|
+
let Some(line) = lines.last_mut() else { continue };
|
|
91
|
+
for (space, look) in std::mem::take(&mut pending) {
|
|
92
|
+
let width = look.face.width(&space, size);
|
|
93
|
+
line.push(&space, look, width);
|
|
94
|
+
}
|
|
95
|
+
place_word(&mut lines, word, *look, size, max_width);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
lines
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/// Places a word on the last line, splitting it across lines when it is
|
|
104
|
+
/// wider than a whole line (at least one character per line).
|
|
105
|
+
fn place_word(lines: &mut Vec<Line>, word: &[u8], look: Look, size: f64, max_width: f64) {
|
|
106
|
+
let mut rest = word;
|
|
107
|
+
while !rest.is_empty() {
|
|
108
|
+
let Some(line) = lines.last_mut() else { return };
|
|
109
|
+
let available = max_width - line.width;
|
|
110
|
+
let mut fit = 0;
|
|
111
|
+
let mut width = 0.0;
|
|
112
|
+
for &byte in rest {
|
|
113
|
+
let next = look.face.width(&[byte], size);
|
|
114
|
+
if width + next > available && (fit > 0 || !line.is_empty()) {
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
width += next;
|
|
118
|
+
fit += 1;
|
|
119
|
+
}
|
|
120
|
+
if fit == 0 {
|
|
121
|
+
lines.push(Line::default());
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
line.push(&rest[..fit], look, width);
|
|
125
|
+
rest = &rest[fit..];
|
|
126
|
+
if !rest.is_empty() {
|
|
127
|
+
lines.push(Line::default());
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
enum Token<'a> {
|
|
133
|
+
Word(&'a [u8]),
|
|
134
|
+
Space(&'a [u8]),
|
|
135
|
+
Newline,
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/// Words, runs of spaces and tabs, and newlines.
|
|
139
|
+
fn tokens(bytes: &[u8]) -> impl Iterator<Item = Token<'_>> {
|
|
140
|
+
let mut rest = bytes;
|
|
141
|
+
std::iter::from_fn(move || {
|
|
142
|
+
let first = *rest.first()?;
|
|
143
|
+
let kind = |byte: u8| match byte {
|
|
144
|
+
b'\n' => 0,
|
|
145
|
+
b' ' | b'\t' => 1,
|
|
146
|
+
_ => 2,
|
|
147
|
+
};
|
|
148
|
+
let length = if first == b'\n' {
|
|
149
|
+
1
|
|
150
|
+
} else {
|
|
151
|
+
rest.iter().position(|&byte| kind(byte) != kind(first)).unwrap_or(rest.len())
|
|
152
|
+
};
|
|
153
|
+
let (token, tail) = rest.split_at(length);
|
|
154
|
+
rest = tail;
|
|
155
|
+
Some(match kind(first) {
|
|
156
|
+
0 => Token::Newline,
|
|
157
|
+
1 => Token::Space(token),
|
|
158
|
+
_ => Token::Word(token),
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
//! Places the document on pages: one content stream per page.
|
|
2
|
+
|
|
3
|
+
#[cfg(test)]
|
|
4
|
+
mod tests;
|
|
5
|
+
mod wrap;
|
|
6
|
+
|
|
7
|
+
use std::fmt::{self, Arguments, Write as _};
|
|
8
|
+
|
|
9
|
+
use super::font::Face;
|
|
10
|
+
use super::objects::{Num, push_string};
|
|
11
|
+
use crate::doc::{PageSize, Paragraph, Table};
|
|
12
|
+
use crate::style::{Align, Color};
|
|
13
|
+
use crate::{Error, Result};
|
|
14
|
+
use wrap::{Line, Look, wrap};
|
|
15
|
+
|
|
16
|
+
/// One inch on every side.
|
|
17
|
+
const MARGIN: f64 = 72.0;
|
|
18
|
+
const BODY_SIZE: f64 = 11.0;
|
|
19
|
+
const TABLE_SIZE: f64 = 10.0;
|
|
20
|
+
/// Line height as a multiple of the font size.
|
|
21
|
+
const LEADING: f64 = 1.25;
|
|
22
|
+
const PARAGRAPH_GAP: f64 = 6.0;
|
|
23
|
+
const CELL_PADDING: f64 = 4.0;
|
|
24
|
+
const HEADING_COLOR: Color = Color::rgb(0x001F_3864);
|
|
25
|
+
const HEADER_FILL: Color = Color::rgb(0x00D9_E2F3);
|
|
26
|
+
const FOOTER_SIZE: f64 = 9.0;
|
|
27
|
+
const FOOTER_COLOR: Color = Color::rgb(0x0066_6666);
|
|
28
|
+
/// Size, space before, color of headings 1 to 3.
|
|
29
|
+
const HEADINGS: [(f64, f64, Option<Color>); 3] =
|
|
30
|
+
[(20.0, 14.0, Some(HEADING_COLOR)), (15.0, 10.0, Some(HEADING_COLOR)), (12.5, 8.0, None)];
|
|
31
|
+
|
|
32
|
+
pub struct Layout {
|
|
33
|
+
width: f64,
|
|
34
|
+
height: f64,
|
|
35
|
+
pages: Vec<Vec<u8>>,
|
|
36
|
+
/// The top of the free space on the current page, from the bottom edge.
|
|
37
|
+
y: f64,
|
|
38
|
+
blank: bool,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
impl Layout {
|
|
42
|
+
pub fn new(size: PageSize) -> Self {
|
|
43
|
+
let (width, height) = size.points();
|
|
44
|
+
Self { width, height, pages: vec![Vec::new()], y: height - MARGIN, blank: true }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
pub const fn page_size(&self) -> (f64, f64) {
|
|
48
|
+
(self.width, self.height)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const fn content_width(&self) -> f64 {
|
|
52
|
+
(-2.0f64).mul_add(MARGIN, self.width)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
pub fn heading(&mut self, level: u8, text: &str) -> Result<()> {
|
|
56
|
+
let index = usize::from(level.clamp(1, 3) - 1);
|
|
57
|
+
let (size, before, color) = HEADINGS[index];
|
|
58
|
+
let look = Look { face: Face::Bold, color, underline: false };
|
|
59
|
+
let lines = wrap(&[(Face::Bold.encode(text)?, look)], size, self.content_width());
|
|
60
|
+
// Keep the heading with at least two lines of what follows.
|
|
61
|
+
let body_lines = 2.0 * BODY_SIZE * LEADING;
|
|
62
|
+
let needed = (line_count(&lines) * size).mul_add(LEADING, before) + body_lines;
|
|
63
|
+
self.ensure_room(needed);
|
|
64
|
+
if !self.blank {
|
|
65
|
+
self.y -= before;
|
|
66
|
+
}
|
|
67
|
+
self.lines(&lines, size, Align::Left);
|
|
68
|
+
self.y -= PARAGRAPH_GAP;
|
|
69
|
+
Ok(())
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
pub fn paragraph(&mut self, paragraph: &Paragraph) -> Result<()> {
|
|
73
|
+
let mut runs = Vec::with_capacity(paragraph.runs().len());
|
|
74
|
+
for run in paragraph.runs() {
|
|
75
|
+
let face = Face::new(run.bold, run.italic);
|
|
76
|
+
runs.push((
|
|
77
|
+
face.encode(&run.text)?,
|
|
78
|
+
Look { face, color: run.color, underline: run.underline },
|
|
79
|
+
));
|
|
80
|
+
}
|
|
81
|
+
let lines = wrap(&runs, BODY_SIZE, self.content_width());
|
|
82
|
+
self.lines(&lines, BODY_SIZE, paragraph.effective_align());
|
|
83
|
+
self.y -= PARAGRAPH_GAP;
|
|
84
|
+
Ok(())
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
pub fn table(&mut self, table: &Table) -> Result<()> {
|
|
88
|
+
let columns = table.columns();
|
|
89
|
+
#[expect(clippy::cast_precision_loss, reason = "a column count is small")]
|
|
90
|
+
let column_width = self.content_width() / columns as f64;
|
|
91
|
+
let mut rows = Vec::with_capacity(table.rows().len());
|
|
92
|
+
for (index, row) in table.rows().iter().enumerate() {
|
|
93
|
+
let header = index == 0 && table.has_header();
|
|
94
|
+
let face = if header { Face::Bold } else { Face::Regular };
|
|
95
|
+
let look = Look { face, color: None, underline: false };
|
|
96
|
+
let mut cells = Vec::with_capacity(columns);
|
|
97
|
+
for column in 0..columns {
|
|
98
|
+
let text = row.get(column).map_or("", String::as_str);
|
|
99
|
+
let runs = [(face.encode(text)?, look)];
|
|
100
|
+
cells.push(wrap(&runs, TABLE_SIZE, (-2.0f64).mul_add(CELL_PADDING, column_width)));
|
|
101
|
+
}
|
|
102
|
+
let tallest = cells.iter().map(|lines| line_count(lines)).fold(1.0, f64::max);
|
|
103
|
+
let height = (tallest * TABLE_SIZE).mul_add(LEADING, 2.0 * CELL_PADDING);
|
|
104
|
+
rows.push((cells, height, header));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let header_height = rows.first().filter(|row| row.2).map_or(0.0, |row| row.1);
|
|
108
|
+
let room = (-2.0f64).mul_add(MARGIN, self.height) - header_height;
|
|
109
|
+
for (index, (cells, height, header)) in rows.iter().enumerate() {
|
|
110
|
+
(*height <= room || *header).ok_or(Error::RowTooTall(*height))?;
|
|
111
|
+
// A header needs the first body row beside it.
|
|
112
|
+
let needed =
|
|
113
|
+
if *header { height + rows.get(1).map_or(0.0, |row| row.1) } else { *height };
|
|
114
|
+
if self.ensure_room(needed) && table.has_header() && index > 0 {
|
|
115
|
+
let (cells, height, _) = &rows[0];
|
|
116
|
+
self.row(cells, *height, column_width, true);
|
|
117
|
+
}
|
|
118
|
+
self.row(cells, *height, column_width, *header);
|
|
119
|
+
}
|
|
120
|
+
self.y -= PARAGRAPH_GAP;
|
|
121
|
+
Ok(())
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
pub fn page_break(&mut self) {
|
|
125
|
+
self.new_page();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/// The finished pages, numbered `1 / N` at the bottom when asked.
|
|
129
|
+
pub fn finish(mut self, page_numbers: bool) -> Vec<Vec<u8>> {
|
|
130
|
+
if page_numbers {
|
|
131
|
+
let total = self.pages.len();
|
|
132
|
+
let center = self.width / 2.0;
|
|
133
|
+
for (index, page) in self.pages.iter_mut().enumerate() {
|
|
134
|
+
let bytes = format!("{} / {total}", index + 1).into_bytes();
|
|
135
|
+
let width = Face::Regular.width(&bytes, FOOTER_SIZE);
|
|
136
|
+
let look =
|
|
137
|
+
Look { face: Face::Regular, color: Some(FOOTER_COLOR), underline: false };
|
|
138
|
+
let label = wrap::Fragment { bytes, look, width };
|
|
139
|
+
draw_fragment(page, &label, FOOTER_SIZE, center - width / 2.0, MARGIN / 2.0);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
self.pages
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/// Starts a new page unless `needed` points fit above the bottom margin
|
|
146
|
+
/// (or the page is still blank, where nothing would fit better).
|
|
147
|
+
/// Returns whether it did.
|
|
148
|
+
fn ensure_room(&mut self, needed: f64) -> bool {
|
|
149
|
+
let full = !self.blank && self.y - needed < MARGIN;
|
|
150
|
+
if full {
|
|
151
|
+
self.new_page();
|
|
152
|
+
}
|
|
153
|
+
full
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
fn new_page(&mut self) {
|
|
157
|
+
self.pages.push(Vec::new());
|
|
158
|
+
self.y = self.height - MARGIN;
|
|
159
|
+
self.blank = true;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
fn current(&mut self) -> &mut Vec<u8> {
|
|
163
|
+
if self.pages.is_empty() {
|
|
164
|
+
self.pages.push(Vec::new());
|
|
165
|
+
}
|
|
166
|
+
let last = self.pages.len() - 1;
|
|
167
|
+
&mut self.pages[last]
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/// Draws lines from the cursor down, starting new pages as needed.
|
|
171
|
+
fn lines(&mut self, lines: &[Line], size: f64, align: Align) {
|
|
172
|
+
let leading = size * LEADING;
|
|
173
|
+
for line in lines {
|
|
174
|
+
self.ensure_room(leading);
|
|
175
|
+
let baseline = self.y - baseline_offset(size);
|
|
176
|
+
let slack = self.content_width() - line.width;
|
|
177
|
+
let x = MARGIN
|
|
178
|
+
+ match align {
|
|
179
|
+
Align::Left => 0.0,
|
|
180
|
+
Align::Center => slack / 2.0,
|
|
181
|
+
Align::Right => slack,
|
|
182
|
+
};
|
|
183
|
+
draw_line(self.current(), line, size, x, baseline);
|
|
184
|
+
self.y -= leading;
|
|
185
|
+
self.blank = false;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/// One table row from the cursor down: fill, borders, then text.
|
|
190
|
+
fn row(&mut self, cells: &[Vec<Line>], height: f64, column_width: f64, header: bool) {
|
|
191
|
+
let top = self.y;
|
|
192
|
+
let page = self.current();
|
|
193
|
+
let mut x = MARGIN;
|
|
194
|
+
for lines in cells {
|
|
195
|
+
let rect = format_args!(
|
|
196
|
+
"{} {} {} {} re",
|
|
197
|
+
Num(x),
|
|
198
|
+
Num(top - height),
|
|
199
|
+
Num(column_width),
|
|
200
|
+
Num(height)
|
|
201
|
+
);
|
|
202
|
+
if header {
|
|
203
|
+
emit(page, format_args!("{} {rect} f\n", Fill(HEADER_FILL)));
|
|
204
|
+
}
|
|
205
|
+
emit(page, format_args!("0.5 w 0 0 0 RG {rect} S\n"));
|
|
206
|
+
let mut y = top - CELL_PADDING;
|
|
207
|
+
for line in lines {
|
|
208
|
+
let baseline = y - baseline_offset(TABLE_SIZE);
|
|
209
|
+
draw_line(page, line, TABLE_SIZE, x + CELL_PADDING, baseline);
|
|
210
|
+
y = TABLE_SIZE.mul_add(-LEADING, y);
|
|
211
|
+
}
|
|
212
|
+
x += column_width;
|
|
213
|
+
}
|
|
214
|
+
self.y -= height;
|
|
215
|
+
self.blank = false;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
#[expect(clippy::cast_precision_loss, reason = "a paragraph has far fewer than 2**53 lines")]
|
|
220
|
+
const fn line_count(lines: &[Line]) -> f64 {
|
|
221
|
+
lines.len() as f64
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/// From the top of a line to its baseline: half the leading the glyphs do
|
|
225
|
+
/// not fill, then the ascender.
|
|
226
|
+
fn baseline_offset(size: f64) -> f64 {
|
|
227
|
+
let (ascent, descent) = (Face::Regular.ascent(size), Face::Regular.descent(size));
|
|
228
|
+
size.mul_add(LEADING, -ascent - descent).mul_add(0.5, ascent)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/// Draws a line's fragments left to right from `x`.
|
|
232
|
+
fn draw_line(page: &mut Vec<u8>, line: &Line, size: f64, mut x: f64, baseline: f64) {
|
|
233
|
+
for fragment in &line.fragments {
|
|
234
|
+
draw_fragment(page, fragment, size, x, baseline);
|
|
235
|
+
x += fragment.width;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
fn draw_fragment(page: &mut Vec<u8>, fragment: &wrap::Fragment, size: f64, x: f64, baseline: f64) {
|
|
240
|
+
let look = fragment.look;
|
|
241
|
+
let color = Fill(look.color.unwrap_or(Color::BLACK));
|
|
242
|
+
let (face, size_num) = (look.face.resource(), Num(size));
|
|
243
|
+
emit(page, format_args!("BT /{face} {size_num} Tf {color} {} {} Td ", Num(x), Num(baseline)));
|
|
244
|
+
push_string(page, &fragment.bytes);
|
|
245
|
+
page.extend_from_slice(b" Tj ET\n");
|
|
246
|
+
if look.underline && fragment.width > 0.0 {
|
|
247
|
+
// Helvetica's underline: 100 below the baseline, 50 thick.
|
|
248
|
+
let (y, thickness) = ((-0.125f64).mul_add(size, baseline), 0.05 * size);
|
|
249
|
+
let (x, y, width, thickness) = (Num(x), Num(y), Num(fragment.width), Num(thickness));
|
|
250
|
+
emit(page, format_args!("{color} {x} {y} {width} {thickness} re f\n"));
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/// `r g b rg`, the fill color operator.
|
|
255
|
+
#[derive(Clone, Copy)]
|
|
256
|
+
struct Fill(Color);
|
|
257
|
+
|
|
258
|
+
impl fmt::Display for Fill {
|
|
259
|
+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
260
|
+
let [r, g, b] = self.0.components();
|
|
261
|
+
write!(f, "{} {} {} rg", Num(r), Num(g), Num(b))
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/// Appends operators to a content stream, formatted in place.
|
|
266
|
+
fn emit(page: &mut Vec<u8>, ops: Arguments<'_>) {
|
|
267
|
+
// `Ops` never fails and every value formatted here is a number, a name
|
|
268
|
+
// or a `Fill`, so formatting cannot fail either.
|
|
269
|
+
let _ = Ops(page).write_fmt(ops);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/// `fmt::Write` over a content stream.
|
|
273
|
+
struct Ops<'a>(&'a mut Vec<u8>);
|
|
274
|
+
|
|
275
|
+
impl fmt::Write for Ops<'_> {
|
|
276
|
+
fn write_str(&mut self, text: &str) -> fmt::Result {
|
|
277
|
+
self.0.extend_from_slice(text.as_bytes());
|
|
278
|
+
Ok(())
|
|
279
|
+
}
|
|
280
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
//! Advance widths and vertical metrics of the PDF standard fonts, in
|
|
2
|
+
//! thousandths of the font size, indexed by `WinAnsiEncoding` code.
|
|
3
|
+
//!
|
|
4
|
+
//! Generated by `tools/generate_pdf_metrics.rb`; do not edit. Derived
|
|
5
|
+
//! (modified: reduced to widths and vertical metrics) from Adobe's Core 14
|
|
6
|
+
//! AFM files, which carry these notices:
|
|
7
|
+
//!
|
|
8
|
+
//! Copyright (c) 1985, 1987, 1989, 1990, 1997 Adobe Systems Incorporated. All
|
|
9
|
+
//! Rights Reserved.
|
|
10
|
+
//!
|
|
11
|
+
//! This file and the 14 PostScript(R) AFM files it accompanies may be used,
|
|
12
|
+
//! copied, and distributed for any purpose and without charge, with or without
|
|
13
|
+
//! modification, provided that all copyright notices are retained; that the AFM
|
|
14
|
+
//! files are not distributed without this file; that all modifications to this
|
|
15
|
+
//! file or any of the AFM files are prominently noted in the modified file(s);
|
|
16
|
+
//! and that this paragraph is not modified. Adobe Systems has no responsibility
|
|
17
|
+
//! or obligation to support the use of the AFM files.
|
|
18
|
+
|
|
19
|
+
/// Metrics of one standard font.
|
|
20
|
+
pub struct FontMetrics {
|
|
21
|
+
pub name: &'static str,
|
|
22
|
+
pub ascender: i16,
|
|
23
|
+
pub descender: i16,
|
|
24
|
+
/// 0 where `WinAnsiEncoding` has no glyph.
|
|
25
|
+
pub widths: [u16; 256],
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// The Unicode character Windows-1252 assigns to bytes 0x80..=0x9F.
|
|
29
|
+
pub const WIN_ANSI_HIGH: [Option<char>; 32] = [
|
|
30
|
+
Some('\u{20ac}'), None, Some('\u{201a}'), Some('\u{192}'),
|
|
31
|
+
Some('\u{201e}'), Some('\u{2026}'), Some('\u{2020}'), Some('\u{2021}'),
|
|
32
|
+
Some('\u{2c6}'), Some('\u{2030}'), Some('\u{160}'), Some('\u{2039}'),
|
|
33
|
+
Some('\u{152}'), None, Some('\u{17d}'), None,
|
|
34
|
+
None, Some('\u{2018}'), Some('\u{2019}'), Some('\u{201c}'),
|
|
35
|
+
Some('\u{201d}'), Some('\u{2022}'), Some('\u{2013}'), Some('\u{2014}'),
|
|
36
|
+
Some('\u{2dc}'), Some('\u{2122}'), Some('\u{161}'), Some('\u{203a}'),
|
|
37
|
+
Some('\u{153}'), None, Some('\u{17e}'), Some('\u{178}'),
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
pub const HELVETICA: FontMetrics = FontMetrics {
|
|
41
|
+
name: "Helvetica",
|
|
42
|
+
ascender: 718,
|
|
43
|
+
descender: -207,
|
|
44
|
+
#[rustfmt::skip]
|
|
45
|
+
widths: [
|
|
46
|
+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
47
|
+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
48
|
+
278, 278, 355, 556, 556, 889, 667, 191, 333, 333, 389, 584, 278, 333, 278, 278,
|
|
49
|
+
556, 556, 556, 556, 556, 556, 556, 556, 556, 556, 278, 278, 584, 584, 584, 556,
|
|
50
|
+
1015, 667, 667, 722, 722, 667, 611, 778, 722, 278, 500, 667, 556, 833, 722, 778,
|
|
51
|
+
667, 778, 722, 667, 611, 722, 667, 944, 667, 667, 611, 278, 278, 278, 469, 556,
|
|
52
|
+
333, 556, 556, 500, 556, 556, 278, 556, 556, 222, 222, 500, 222, 833, 556, 556,
|
|
53
|
+
556, 556, 333, 500, 278, 556, 500, 722, 500, 500, 500, 334, 260, 334, 584, 350,
|
|
54
|
+
556, 350, 222, 556, 333, 1000, 556, 556, 333, 1000, 667, 333, 1000, 350, 611, 350,
|
|
55
|
+
350, 222, 222, 333, 333, 350, 556, 1000, 333, 1000, 500, 333, 944, 350, 500, 667,
|
|
56
|
+
278, 333, 556, 556, 556, 556, 260, 556, 333, 737, 370, 556, 584, 333, 737, 333,
|
|
57
|
+
400, 584, 333, 333, 333, 556, 537, 278, 333, 333, 365, 556, 834, 834, 834, 611,
|
|
58
|
+
667, 667, 667, 667, 667, 667, 1000, 722, 667, 667, 667, 667, 278, 278, 278, 278,
|
|
59
|
+
722, 722, 778, 778, 778, 778, 778, 584, 778, 722, 722, 722, 722, 667, 667, 611,
|
|
60
|
+
556, 556, 556, 556, 556, 556, 889, 500, 556, 556, 556, 556, 278, 278, 278, 278,
|
|
61
|
+
556, 556, 556, 556, 556, 556, 556, 584, 611, 556, 556, 556, 556, 500, 556, 500,
|
|
62
|
+
],
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
pub const HELVETICA_BOLD: FontMetrics = FontMetrics {
|
|
66
|
+
name: "Helvetica-Bold",
|
|
67
|
+
ascender: 718,
|
|
68
|
+
descender: -207,
|
|
69
|
+
#[rustfmt::skip]
|
|
70
|
+
widths: [
|
|
71
|
+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
72
|
+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
73
|
+
278, 333, 474, 556, 556, 889, 722, 238, 333, 333, 389, 584, 278, 333, 278, 278,
|
|
74
|
+
556, 556, 556, 556, 556, 556, 556, 556, 556, 556, 333, 333, 584, 584, 584, 611,
|
|
75
|
+
975, 722, 722, 722, 722, 667, 611, 778, 722, 278, 556, 722, 611, 833, 722, 778,
|
|
76
|
+
667, 778, 722, 667, 611, 722, 667, 944, 667, 667, 611, 333, 278, 333, 584, 556,
|
|
77
|
+
333, 556, 611, 556, 611, 556, 333, 611, 611, 278, 278, 556, 278, 889, 611, 611,
|
|
78
|
+
611, 611, 389, 556, 333, 611, 556, 778, 556, 556, 500, 389, 280, 389, 584, 350,
|
|
79
|
+
556, 350, 278, 556, 500, 1000, 556, 556, 333, 1000, 667, 333, 1000, 350, 611, 350,
|
|
80
|
+
350, 278, 278, 500, 500, 350, 556, 1000, 333, 1000, 556, 333, 944, 350, 500, 667,
|
|
81
|
+
278, 333, 556, 556, 556, 556, 280, 556, 333, 737, 370, 556, 584, 333, 737, 333,
|
|
82
|
+
400, 584, 333, 333, 333, 611, 556, 278, 333, 333, 365, 556, 834, 834, 834, 611,
|
|
83
|
+
722, 722, 722, 722, 722, 722, 1000, 722, 667, 667, 667, 667, 278, 278, 278, 278,
|
|
84
|
+
722, 722, 778, 778, 778, 778, 778, 584, 778, 722, 722, 722, 722, 667, 667, 611,
|
|
85
|
+
556, 556, 556, 556, 556, 556, 889, 556, 556, 556, 556, 556, 278, 278, 278, 278,
|
|
86
|
+
611, 611, 611, 611, 611, 611, 611, 584, 611, 611, 611, 611, 611, 556, 611, 556,
|
|
87
|
+
],
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
pub const HELVETICA_OBLIQUE: FontMetrics = FontMetrics {
|
|
91
|
+
name: "Helvetica-Oblique",
|
|
92
|
+
ascender: 718,
|
|
93
|
+
descender: -207,
|
|
94
|
+
#[rustfmt::skip]
|
|
95
|
+
widths: [
|
|
96
|
+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
97
|
+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
98
|
+
278, 278, 355, 556, 556, 889, 667, 191, 333, 333, 389, 584, 278, 333, 278, 278,
|
|
99
|
+
556, 556, 556, 556, 556, 556, 556, 556, 556, 556, 278, 278, 584, 584, 584, 556,
|
|
100
|
+
1015, 667, 667, 722, 722, 667, 611, 778, 722, 278, 500, 667, 556, 833, 722, 778,
|
|
101
|
+
667, 778, 722, 667, 611, 722, 667, 944, 667, 667, 611, 278, 278, 278, 469, 556,
|
|
102
|
+
333, 556, 556, 500, 556, 556, 278, 556, 556, 222, 222, 500, 222, 833, 556, 556,
|
|
103
|
+
556, 556, 333, 500, 278, 556, 500, 722, 500, 500, 500, 334, 260, 334, 584, 350,
|
|
104
|
+
556, 350, 222, 556, 333, 1000, 556, 556, 333, 1000, 667, 333, 1000, 350, 611, 350,
|
|
105
|
+
350, 222, 222, 333, 333, 350, 556, 1000, 333, 1000, 500, 333, 944, 350, 500, 667,
|
|
106
|
+
278, 333, 556, 556, 556, 556, 260, 556, 333, 737, 370, 556, 584, 333, 737, 333,
|
|
107
|
+
400, 584, 333, 333, 333, 556, 537, 278, 333, 333, 365, 556, 834, 834, 834, 611,
|
|
108
|
+
667, 667, 667, 667, 667, 667, 1000, 722, 667, 667, 667, 667, 278, 278, 278, 278,
|
|
109
|
+
722, 722, 778, 778, 778, 778, 778, 584, 778, 722, 722, 722, 722, 667, 667, 611,
|
|
110
|
+
556, 556, 556, 556, 556, 556, 889, 500, 556, 556, 556, 556, 278, 278, 278, 278,
|
|
111
|
+
556, 556, 556, 556, 556, 556, 556, 584, 611, 556, 556, 556, 556, 500, 556, 500,
|
|
112
|
+
],
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
pub const HELVETICA_BOLDOBLIQUE: FontMetrics = FontMetrics {
|
|
116
|
+
name: "Helvetica-BoldOblique",
|
|
117
|
+
ascender: 718,
|
|
118
|
+
descender: -207,
|
|
119
|
+
#[rustfmt::skip]
|
|
120
|
+
widths: [
|
|
121
|
+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
122
|
+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
123
|
+
278, 333, 474, 556, 556, 889, 722, 238, 333, 333, 389, 584, 278, 333, 278, 278,
|
|
124
|
+
556, 556, 556, 556, 556, 556, 556, 556, 556, 556, 333, 333, 584, 584, 584, 611,
|
|
125
|
+
975, 722, 722, 722, 722, 667, 611, 778, 722, 278, 556, 722, 611, 833, 722, 778,
|
|
126
|
+
667, 778, 722, 667, 611, 722, 667, 944, 667, 667, 611, 333, 278, 333, 584, 556,
|
|
127
|
+
333, 556, 611, 556, 611, 556, 333, 611, 611, 278, 278, 556, 278, 889, 611, 611,
|
|
128
|
+
611, 611, 389, 556, 333, 611, 556, 778, 556, 556, 500, 389, 280, 389, 584, 350,
|
|
129
|
+
556, 350, 278, 556, 500, 1000, 556, 556, 333, 1000, 667, 333, 1000, 350, 611, 350,
|
|
130
|
+
350, 278, 278, 500, 500, 350, 556, 1000, 333, 1000, 556, 333, 944, 350, 500, 667,
|
|
131
|
+
278, 333, 556, 556, 556, 556, 280, 556, 333, 737, 370, 556, 584, 333, 737, 333,
|
|
132
|
+
400, 584, 333, 333, 333, 611, 556, 278, 333, 333, 365, 556, 834, 834, 834, 611,
|
|
133
|
+
722, 722, 722, 722, 722, 722, 1000, 722, 667, 667, 667, 667, 278, 278, 278, 278,
|
|
134
|
+
722, 722, 778, 778, 778, 778, 778, 584, 778, 722, 722, 722, 722, 667, 667, 611,
|
|
135
|
+
556, 556, 556, 556, 556, 556, 889, 556, 556, 556, 556, 556, 278, 278, 278, 278,
|
|
136
|
+
611, 611, 611, 611, 611, 611, 611, 584, 611, 611, 611, 611, 611, 556, 611, 556,
|
|
137
|
+
],
|
|
138
|
+
};
|
|
139
|
+
|