imprimatur 0.3.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 +9 -0
- data/Cargo.lock +33 -3
- data/Cargo.toml +1 -1
- data/README.md +41 -1
- data/ext/imprimatur_native/cli/Cargo.toml +3 -1
- data/ext/imprimatur_native/core/Cargo.toml +15 -4
- data/ext/imprimatur_native/core/README.md +27 -4
- 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/symbol.rs +136 -0
- data/ext/imprimatur_native/core/src/doc.rs +11 -1
- data/ext/imprimatur_native/core/src/docx.rs +22 -10
- data/ext/imprimatur_native/core/src/error.rs +6 -0
- data/ext/imprimatur_native/core/src/font.rs +3 -1
- data/ext/imprimatur_native/core/src/lib.rs +3 -1
- data/ext/imprimatur_native/core/src/pdf/layout.rs +48 -14
- 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 +9 -1
- data/ext/imprimatur_native/core/src/read/image.rs +17 -0
- data/ext/imprimatur_native/core/src/read.rs +3 -2
- data/ext/imprimatur_native/core/src/style.rs +11 -0
- data/ext/imprimatur_native/ffi/Cargo.toml +1 -1
- data/ext/imprimatur_native/ffi/src/barcode.rs +147 -0
- data/ext/imprimatur_native/ffi/src/document.rs +29 -1
- data/ext/imprimatur_native/ffi/src/lib.rs +5 -3
- data/lib/imprimatur/barcode.rb +78 -0
- data/lib/imprimatur/document.rb +17 -1
- data/lib/imprimatur/version.rb +1 -1
- data/lib/imprimatur.rb +4 -0
- metadata +13 -2
- data/ext/imprimatur_native/core/src/test_support.rs +0 -251
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
//! Barcodes in the flow: shapes in a PDF, a picture in DOCX.
|
|
2
|
+
|
|
3
|
+
use crate::Result;
|
|
4
|
+
use crate::barcode::{Barcode, Look, Shapes};
|
|
5
|
+
use crate::doc::Picture;
|
|
6
|
+
use crate::style::{Align, Color};
|
|
7
|
+
|
|
8
|
+
/// The largest side, in points, a symbol may be set to: PDF's page limit.
|
|
9
|
+
const MAX_POINTS: f64 = 14_400.0;
|
|
10
|
+
/// Pixels an inch a DOCX picture of a symbol has at least.
|
|
11
|
+
const DOCX_DPI: f64 = 300.0;
|
|
12
|
+
|
|
13
|
+
/// A [`Barcode`] drawn in a document's flow.
|
|
14
|
+
///
|
|
15
|
+
/// It is drawn at the width or height set (in points; a QR Code or Data
|
|
16
|
+
/// Matrix keeps square modules, a linear code's height is its bars'), or
|
|
17
|
+
/// else at its symbology's usual module size, never larger than the page
|
|
18
|
+
/// leaves room for. Its quiet zone is part of its size.
|
|
19
|
+
///
|
|
20
|
+
/// ```
|
|
21
|
+
/// use imprimatur::barcode::{Barcode, Level};
|
|
22
|
+
/// use imprimatur::doc::{Document, Symbol};
|
|
23
|
+
/// use imprimatur::style::Align;
|
|
24
|
+
///
|
|
25
|
+
/// let mut doc = Document::new();
|
|
26
|
+
/// let qr = Barcode::qr("https://example.com", Level::Medium)?;
|
|
27
|
+
/// doc.barcode(Symbol::new(qr).width(144.0)?.align(Align::Center));
|
|
28
|
+
/// doc.barcode(Barcode::ean13("950600013435")?);
|
|
29
|
+
/// assert!(doc.to_pdf()?.starts_with(b"%PDF-"));
|
|
30
|
+
/// # Ok::<(), imprimatur::Error>(())
|
|
31
|
+
/// ```
|
|
32
|
+
#[derive(Debug, Clone, PartialEq)]
|
|
33
|
+
pub struct Symbol {
|
|
34
|
+
pub(crate) barcode: Barcode,
|
|
35
|
+
width: Option<f64>,
|
|
36
|
+
height: Option<f64>,
|
|
37
|
+
pub(crate) align: Option<Align>,
|
|
38
|
+
pub(crate) color: Color,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
impl Symbol {
|
|
42
|
+
#[must_use]
|
|
43
|
+
pub const fn new(barcode: Barcode) -> Self {
|
|
44
|
+
Self { barcode, width: None, height: None, align: None, color: Color::BLACK }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/// Its width in points, quiet zone included.
|
|
48
|
+
///
|
|
49
|
+
/// # Errors
|
|
50
|
+
/// [`Error::InvalidBarcode`](crate::Error::InvalidBarcode) outside 1 to
|
|
51
|
+
/// 14 400 points.
|
|
52
|
+
pub fn width(mut self, points: f64) -> Result<Self> {
|
|
53
|
+
self.width = Some(self.points_in_range(points)?);
|
|
54
|
+
Ok(self)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// Its height in points: a linear code's bars, a QR Code or Data
|
|
58
|
+
/// Matrix's whole (quiet zone included).
|
|
59
|
+
///
|
|
60
|
+
/// # Errors
|
|
61
|
+
/// [`Error::InvalidBarcode`](crate::Error::InvalidBarcode) outside 1 to
|
|
62
|
+
/// 14 400 points.
|
|
63
|
+
pub fn height(mut self, points: f64) -> Result<Self> {
|
|
64
|
+
self.height = Some(self.points_in_range(points)?);
|
|
65
|
+
Ok(self)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
#[must_use]
|
|
69
|
+
pub const fn align(mut self, align: Align) -> Self {
|
|
70
|
+
self.align = Some(align);
|
|
71
|
+
self
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/// The bars' color; black unless set.
|
|
75
|
+
#[must_use]
|
|
76
|
+
pub const fn color(mut self, color: Color) -> Self {
|
|
77
|
+
self.color = color;
|
|
78
|
+
self
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/// Its rectangles and the size they are drawn at, in points, fitted
|
|
82
|
+
/// into `room` (the width and height between the margins).
|
|
83
|
+
pub(crate) fn placed(&self, room: (f64, f64)) -> (Shapes, (f64, f64)) {
|
|
84
|
+
let shapes = self.barcode.shapes(true, None);
|
|
85
|
+
let (columns, rows) = shapes.extent();
|
|
86
|
+
let module = self.barcode.default_module();
|
|
87
|
+
let (width, height) = if self.barcode.symbology().is_linear() {
|
|
88
|
+
let width = self.width.unwrap_or(columns * module);
|
|
89
|
+
(width, self.height.unwrap_or(rows * width / columns))
|
|
90
|
+
} else {
|
|
91
|
+
let module = match (self.width, self.height) {
|
|
92
|
+
(Some(width), Some(height)) => (width / columns).min(height / rows),
|
|
93
|
+
(Some(width), None) => width / columns,
|
|
94
|
+
(None, Some(height)) => height / rows,
|
|
95
|
+
(None, None) => module,
|
|
96
|
+
};
|
|
97
|
+
(columns * module, rows * module)
|
|
98
|
+
};
|
|
99
|
+
let scale = (room.0 / width).min(room.1 / height).min(1.0);
|
|
100
|
+
(shapes, (width * scale, height * scale))
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/// A PNG of it at 300 pixels an inch or more, for formats that take
|
|
104
|
+
/// pictures only.
|
|
105
|
+
pub(crate) fn picture(&self, room: (f64, f64)) -> Result<Picture> {
|
|
106
|
+
let (shapes, (width, height)) = self.placed(room);
|
|
107
|
+
let module = width / shapes.extent().0;
|
|
108
|
+
#[expect(clippy::cast_possible_truncation, clippy::cast_sign_loss, reason = "1 to 64")]
|
|
109
|
+
let scale = (module * DOCX_DPI / 72.0).ceil().clamp(1.0, 64.0) as u32;
|
|
110
|
+
#[expect(clippy::cast_possible_truncation, clippy::cast_sign_loss, reason = "bars, small")]
|
|
111
|
+
let bar_height = (height / module).round().max(1.0) as u32;
|
|
112
|
+
let look = Look {
|
|
113
|
+
dark: self.color,
|
|
114
|
+
light: Some(Color::WHITE),
|
|
115
|
+
quiet_zone: true,
|
|
116
|
+
bar_height: Some(bar_height),
|
|
117
|
+
};
|
|
118
|
+
let png = self.barcode.png(scale, &look)?;
|
|
119
|
+
let picture = Picture::new(png)?.width(width)?.height(height)?;
|
|
120
|
+
Ok(match self.align {
|
|
121
|
+
Some(align) => picture.align(align),
|
|
122
|
+
None => picture,
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
fn points_in_range(&self, points: f64) -> Result<f64> {
|
|
127
|
+
let invalid = || self.barcode.symbology().invalid("a size of 1 to 14 400 points");
|
|
128
|
+
(1.0..=MAX_POINTS).contains(&points).then_some(points).ok_or_else(invalid)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
impl From<Barcode> for Symbol {
|
|
133
|
+
fn from(barcode: Barcode) -> Self {
|
|
134
|
+
Self::new(barcode)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
|
|
24
24
|
mod paragraph;
|
|
25
25
|
pub(crate) mod picture;
|
|
26
|
+
mod symbol;
|
|
26
27
|
mod table;
|
|
27
28
|
#[cfg(test)]
|
|
28
29
|
mod tests;
|
|
@@ -33,6 +34,7 @@ use std::path::Path;
|
|
|
33
34
|
|
|
34
35
|
pub use paragraph::{Paragraph, Run};
|
|
35
36
|
pub use picture::Picture;
|
|
37
|
+
pub use symbol::Symbol;
|
|
36
38
|
pub use table::Table;
|
|
37
39
|
|
|
38
40
|
use crate::script::Counts;
|
|
@@ -96,6 +98,7 @@ pub enum Block {
|
|
|
96
98
|
Paragraph(Paragraph),
|
|
97
99
|
Table(Table),
|
|
98
100
|
Picture(Picture),
|
|
101
|
+
Symbol(Symbol),
|
|
99
102
|
PageBreak,
|
|
100
103
|
}
|
|
101
104
|
|
|
@@ -158,6 +161,13 @@ impl Document {
|
|
|
158
161
|
self
|
|
159
162
|
}
|
|
160
163
|
|
|
164
|
+
/// A barcode, in the flow of the text: a [`Barcode`](crate::barcode::Barcode)
|
|
165
|
+
/// at its usual size, or a [`Symbol`] sized and aligned.
|
|
166
|
+
pub fn barcode(&mut self, symbol: impl Into<Symbol>) -> &mut Self {
|
|
167
|
+
self.blocks.push(Block::Symbol(symbol.into()));
|
|
168
|
+
self
|
|
169
|
+
}
|
|
170
|
+
|
|
161
171
|
/// Numbers pages `1 / 3` at the bottom of each one.
|
|
162
172
|
pub const fn set_page_numbers(&mut self, enabled: bool) -> &mut Self {
|
|
163
173
|
self.page_numbers = enabled;
|
|
@@ -222,7 +232,7 @@ impl Document {
|
|
|
222
232
|
paragraph.runs.iter().for_each(|run| counts.add(&run.text));
|
|
223
233
|
}
|
|
224
234
|
Block::Table(table) => table.count(&mut counts),
|
|
225
|
-
Block::Picture(_) | Block::PageBreak => {}
|
|
235
|
+
Block::Picture(_) | Block::Symbol(_) | Block::PageBreak => {}
|
|
226
236
|
}
|
|
227
237
|
}
|
|
228
238
|
counts.right_to_left()
|
|
@@ -6,6 +6,7 @@ mod picture;
|
|
|
6
6
|
#[cfg(test)]
|
|
7
7
|
mod tests;
|
|
8
8
|
|
|
9
|
+
use std::borrow::Cow;
|
|
9
10
|
use std::io::Write;
|
|
10
11
|
|
|
11
12
|
use crate::Result;
|
|
@@ -29,11 +30,17 @@ pub fn write<W: Write>(document: &Document, out: W) -> Result<W> {
|
|
|
29
30
|
}
|
|
30
31
|
// Pictures' files follow, their relationships numbered from here.
|
|
31
32
|
let first_media = parts.len() + 1;
|
|
32
|
-
|
|
33
|
+
// Barcodes go in as pictures.
|
|
34
|
+
let rendered = document
|
|
33
35
|
.blocks()
|
|
34
36
|
.iter()
|
|
35
|
-
.filter_map(|block|
|
|
36
|
-
|
|
37
|
+
.filter_map(|block| match block {
|
|
38
|
+
Block::Picture(picture) => Some(Ok(Cow::Borrowed(picture))),
|
|
39
|
+
Block::Symbol(symbol) => Some(symbol.picture(room).map(Cow::Owned)),
|
|
40
|
+
_ => None,
|
|
41
|
+
})
|
|
42
|
+
.collect::<Result<Vec<Cow<'_, Picture>>>>()?;
|
|
43
|
+
let pictures: Vec<&Picture> = rendered.iter().map(|picture| &**picture).collect();
|
|
37
44
|
for (number, picture) in (1..).zip(&pictures) {
|
|
38
45
|
parts.push(("image", picture::file_name(picture, number)));
|
|
39
46
|
}
|
|
@@ -41,7 +48,7 @@ pub fn write<W: Write>(document: &Document, out: W) -> Result<W> {
|
|
|
41
48
|
zip.add("[Content_Types].xml", content_types(footer, &pictures).as_bytes())?;
|
|
42
49
|
let package = opc::relationships(&[("officeDocument", "word/document.xml")]);
|
|
43
50
|
zip.add("_rels/.rels", package.as_bytes())?;
|
|
44
|
-
zip.add("word/document.xml", body(document, room, first_media).as_bytes())?;
|
|
51
|
+
zip.add("word/document.xml", body(document, room, first_media, &pictures).as_bytes())?;
|
|
45
52
|
zip.add("word/_rels/document.xml.rels", opc::relationships(&parts).as_bytes())?;
|
|
46
53
|
zip.add("word/styles.xml", styles().as_bytes())?;
|
|
47
54
|
if footer {
|
|
@@ -102,7 +109,12 @@ fn page_number_footer() -> String {
|
|
|
102
109
|
xml.finish()
|
|
103
110
|
}
|
|
104
111
|
|
|
105
|
-
fn body(
|
|
112
|
+
fn body(
|
|
113
|
+
document: &Document,
|
|
114
|
+
room: (f64, f64),
|
|
115
|
+
first_media: usize,
|
|
116
|
+
pictures: &[&Picture],
|
|
117
|
+
) -> String {
|
|
106
118
|
// What reads in no direction of its own (figures, punctuation) takes the
|
|
107
119
|
// document's.
|
|
108
120
|
let rtl = document.reads_right_to_left();
|
|
@@ -116,7 +128,7 @@ fn body(document: &Document, room: (f64, f64), first_media: usize) -> String {
|
|
|
116
128
|
];
|
|
117
129
|
xml.open("w:document", &namespaces);
|
|
118
130
|
xml.open("w:body", &[]);
|
|
119
|
-
let mut
|
|
131
|
+
let mut drawn = 0;
|
|
120
132
|
for block in document.blocks() {
|
|
121
133
|
match block {
|
|
122
134
|
Block::Heading { level, text } => {
|
|
@@ -125,10 +137,10 @@ fn body(document: &Document, room: (f64, f64), first_media: usize) -> String {
|
|
|
125
137
|
}
|
|
126
138
|
Block::Paragraph(content) => paragraph(&mut xml, content, rtl),
|
|
127
139
|
Block::Table(content) => table(&mut xml, content, document, rtl),
|
|
128
|
-
Block::Picture(
|
|
129
|
-
|
|
130
|
-
let relationship = format!("rId{}", first_media +
|
|
131
|
-
picture::paragraph(&mut xml,
|
|
140
|
+
Block::Picture(_) | Block::Symbol(_) => {
|
|
141
|
+
drawn += 1;
|
|
142
|
+
let relationship = format!("rId{}", first_media + drawn - 1);
|
|
143
|
+
picture::paragraph(&mut xml, pictures[drawn - 1], room, drawn, &relationship);
|
|
132
144
|
}
|
|
133
145
|
Block::PageBreak => {
|
|
134
146
|
xml.open("w:p", &[]);
|
|
@@ -88,6 +88,12 @@ pub enum Error {
|
|
|
88
88
|
#[error("a table row needs {0:.0}pt, more than a whole page holds")]
|
|
89
89
|
RowTooTall(f64),
|
|
90
90
|
|
|
91
|
+
#[error("invalid {symbology} barcode: {reason}")]
|
|
92
|
+
InvalidBarcode { symbology: &'static str, reason: String },
|
|
93
|
+
|
|
94
|
+
#[error("invalid GS1 element string: {0}")]
|
|
95
|
+
InvalidElementString(String),
|
|
96
|
+
|
|
91
97
|
#[error("a workbook needs at least one sheet")]
|
|
92
98
|
EmptyWorkbook,
|
|
93
99
|
|
|
@@ -224,6 +224,8 @@ fn unicode_cmap<'a>(cmap: &Cmap<'a>) -> Result<CmapSubtable<'a>> {
|
|
|
224
224
|
Err(Error::UnsupportedFont("no supported Unicode cmap"))
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
-
|
|
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 {
|
|
228
230
|
Error::malformed("TrueType font", reason.to_string())
|
|
229
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,6 +21,7 @@
|
|
|
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;
|
|
@@ -8,7 +8,7 @@ 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::{Document, Paragraph, Picture, Table};
|
|
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};
|
|
@@ -129,20 +129,30 @@ impl Layout {
|
|
|
129
129
|
/// Draws picture `index` (`/Im{index}`), on a new page when it does not
|
|
130
130
|
/// fit, as its alignment says.
|
|
131
131
|
pub fn picture(&mut self, picture: &Picture, index: usize) {
|
|
132
|
-
let
|
|
133
|
-
let (
|
|
134
|
-
|
|
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));
|
|
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));
|
|
143
135
|
emit(self.current(), format_args!("q {width} 0 0 {height} {x} {y} cm /Im{index} Do Q\n"));
|
|
144
|
-
self.
|
|
145
|
-
|
|
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();
|
|
146
156
|
}
|
|
147
157
|
|
|
148
158
|
pub fn page_break(&mut self) {
|
|
@@ -166,6 +176,30 @@ impl Layout {
|
|
|
166
176
|
self.pages
|
|
167
177
|
}
|
|
168
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
|
+
|
|
169
203
|
/// Starts a new page unless `needed` points fit above the bottom margin
|
|
170
204
|
/// (or the page is still blank, where nothing would fit better).
|
|
171
205
|
/// Returns whether it did.
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
//! TrueType metadata needed by a PDF `FontDescriptor`, in 1000-unit glyph space.
|
|
2
|
+
|
|
3
|
+
#[cfg(test)]
|
|
4
|
+
mod tests;
|
|
5
|
+
|
|
6
|
+
use std::io::Write;
|
|
7
|
+
|
|
8
|
+
use read_fonts::{
|
|
9
|
+
FontRead, FontRef, TableProvider,
|
|
10
|
+
tables::{head::MacStyle, os2::Os2, post::Post},
|
|
11
|
+
types::{NameId, Tag},
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
use crate::font::{TrueTypeFont, invalid};
|
|
15
|
+
use crate::pdf::objects::{Num, PdfWriter};
|
|
16
|
+
use crate::{Error, Result};
|
|
17
|
+
|
|
18
|
+
pub struct Descriptor {
|
|
19
|
+
/// PostScript name, escaped as a PDF name (without its leading slash).
|
|
20
|
+
pub name: String,
|
|
21
|
+
bounds: [Num; 4],
|
|
22
|
+
ascent: Num,
|
|
23
|
+
descent: Num,
|
|
24
|
+
cap_height: Num,
|
|
25
|
+
italic_angle: Num,
|
|
26
|
+
flags: u32,
|
|
27
|
+
stem: Num,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
impl Descriptor {
|
|
31
|
+
pub fn read(font: &TrueTypeFont<'_>) -> Result<Self> {
|
|
32
|
+
let tables = FontRef::new(font.as_bytes()).map_err(invalid)?;
|
|
33
|
+
let head = tables.head().map_err(invalid)?;
|
|
34
|
+
let post =
|
|
35
|
+
tables.table_data(Tag::new(b"post")).map(Post::read).transpose().map_err(invalid)?;
|
|
36
|
+
let os2 =
|
|
37
|
+
tables.table_data(Tag::new(b"OS/2")).map(Os2::read).transpose().map_err(invalid)?;
|
|
38
|
+
if let Some(os2) = &os2 {
|
|
39
|
+
let rights = os2.fs_type();
|
|
40
|
+
// Older fonts can set several rights bits; the least restrictive wins.
|
|
41
|
+
(rights & 0x0002 == 0 || rights & 0x000C != 0)
|
|
42
|
+
.ok_or(Error::UnsupportedFont("restricted font embedding"))?;
|
|
43
|
+
(rights & 0x0200 == 0).ok_or(Error::UnsupportedFont("bitmap-only font embedding"))?;
|
|
44
|
+
// The no-subsetting bit is compatible with embedding the complete font.
|
|
45
|
+
}
|
|
46
|
+
(head.x_min() <= head.x_max() && head.y_min() <= head.y_max())
|
|
47
|
+
.ok_or_else(|| invalid("inverted font bounding box"))?;
|
|
48
|
+
let metrics = font.metrics();
|
|
49
|
+
let scale = 1000.0 / f64::from(metrics.units_per_em);
|
|
50
|
+
let cap_height = os2
|
|
51
|
+
.as_ref()
|
|
52
|
+
.and_then(Os2::s_cap_height)
|
|
53
|
+
.filter(|height| *height > 0)
|
|
54
|
+
.unwrap_or(metrics.ascender);
|
|
55
|
+
let angle = post.as_ref().map_or(0.0, |post| post.italic_angle().to_f64());
|
|
56
|
+
// Symbolic: Identity-H is a CID encoding, not the standard Latin encoding.
|
|
57
|
+
let mut flags = 4;
|
|
58
|
+
if post.as_ref().is_some_and(|post| post.is_fixed_pitch() != 0) {
|
|
59
|
+
flags |= 1;
|
|
60
|
+
}
|
|
61
|
+
if angle != 0.0 || head.mac_style().contains(MacStyle::ITALIC) {
|
|
62
|
+
flags |= 64;
|
|
63
|
+
}
|
|
64
|
+
let family = os2.as_ref().map_or(0, |os2| os2.s_family_class() >> 8);
|
|
65
|
+
if (1..=7).contains(&family) {
|
|
66
|
+
flags |= 2;
|
|
67
|
+
} else if family == 10 {
|
|
68
|
+
flags |= 8;
|
|
69
|
+
}
|
|
70
|
+
let weight = os2.as_ref().map_or(400, |os2| os2.us_weight_class().clamp(1, 1000));
|
|
71
|
+
let stem_weight = f64::from(weight) / 65.0;
|
|
72
|
+
Ok(Self {
|
|
73
|
+
name: postscript_name(&tables)?,
|
|
74
|
+
bounds: [head.x_min(), head.y_min(), head.x_max(), head.y_max()]
|
|
75
|
+
.map(|value| Num(f64::from(value) * scale)),
|
|
76
|
+
ascent: Num(f64::from(metrics.ascender) * scale),
|
|
77
|
+
descent: Num(f64::from(metrics.descender) * scale),
|
|
78
|
+
// Older OS/2 tables lack cap height; the ascender is an approximation.
|
|
79
|
+
cap_height: Num(f64::from(cap_height) * scale),
|
|
80
|
+
italic_angle: Num(angle),
|
|
81
|
+
flags,
|
|
82
|
+
// TrueType has no stem-width field: estimate it from the weight class.
|
|
83
|
+
stem: Num(stem_weight.mul_add(stem_weight, 50.0)),
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
pub fn write<W: Write>(&self, pdf: &mut PdfWriter<W>, id: usize, file: usize) -> Result<()> {
|
|
88
|
+
let Self { name, bounds, ascent, descent, cap_height, italic_angle, flags, stem } = self;
|
|
89
|
+
let [left, bottom, right, top] = bounds;
|
|
90
|
+
pdf.object(
|
|
91
|
+
id,
|
|
92
|
+
format_args!(
|
|
93
|
+
"<< /Type /FontDescriptor /FontName /{name} /Flags {flags} \
|
|
94
|
+
/FontBBox [{left} {bottom} {right} {top}] /ItalicAngle {italic_angle} \
|
|
95
|
+
/Ascent {ascent} /Descent {descent} /CapHeight {cap_height} \
|
|
96
|
+
/StemV {stem} /FontFile2 {file} 0 R >>"
|
|
97
|
+
),
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
fn postscript_name(font: &FontRef<'_>) -> Result<String> {
|
|
103
|
+
let names = font.name().map_err(invalid)?;
|
|
104
|
+
for platform in [3, 0, 1] {
|
|
105
|
+
for record in names.name_record() {
|
|
106
|
+
if record.name_id() != NameId::POSTSCRIPT_NAME || record.platform_id() != platform {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if !(record.is_unicode() || platform == 1 && record.encoding_id() == 0) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if record.is_unicode() && !record.length().is_multiple_of(2) {
|
|
113
|
+
return Err(invalid("odd UTF-16 PostScript name length"));
|
|
114
|
+
}
|
|
115
|
+
let name = record.string(names.string_data()).map_err(invalid)?.to_string();
|
|
116
|
+
// OpenType name ID 6: 1..63 printable ASCII characters, excluding
|
|
117
|
+
// PostScript delimiters. '#' is legal there but needs escaping in PDF.
|
|
118
|
+
if !name.is_empty()
|
|
119
|
+
&& name.len() <= 63
|
|
120
|
+
&& name
|
|
121
|
+
.bytes()
|
|
122
|
+
.all(|byte| (33..=126).contains(&byte) && !b"[](){}<>/%".contains(&byte))
|
|
123
|
+
{
|
|
124
|
+
return Ok(name.replace('#', "#23"));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
Err(Error::UnsupportedFont("no usable PostScript font name"))
|
|
129
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
//! A `CIDFont`'s TrueType program retains every glyph but omits the cmap table
|
|
2
|
+
//! (ISO 32000-1, 9.9). Rebuild the sfnt directory, padding and checksums; glyph
|
|
3
|
+
//! outlines, IDs, metrics and hinting tables are unchanged.
|
|
4
|
+
|
|
5
|
+
#[cfg(test)]
|
|
6
|
+
mod tests;
|
|
7
|
+
|
|
8
|
+
use read_fonts::{FontRef, TableRecord, types::Tag};
|
|
9
|
+
|
|
10
|
+
use crate::Result;
|
|
11
|
+
use crate::font::{TrueTypeFont, invalid};
|
|
12
|
+
|
|
13
|
+
pub fn for_pdf(font: &TrueTypeFont<'_>) -> Result<Vec<u8>> {
|
|
14
|
+
let source = FontRef::new(font.as_bytes()).map_err(invalid)?;
|
|
15
|
+
let mut tags: Vec<_> = source
|
|
16
|
+
.table_directory()
|
|
17
|
+
.table_records()
|
|
18
|
+
.iter()
|
|
19
|
+
.map(TableRecord::tag)
|
|
20
|
+
// Rewriting the directory also invalidates any digital signature.
|
|
21
|
+
.filter(|tag| *tag != Tag::new(b"cmap") && *tag != Tag::new(b"DSIG"))
|
|
22
|
+
.collect();
|
|
23
|
+
tags.sort_unstable();
|
|
24
|
+
(!tags.windows(2).any(|pair| pair[0] == pair[1]))
|
|
25
|
+
.ok_or_else(|| invalid("duplicate sfnt tables"))?;
|
|
26
|
+
let count = u16::try_from(tags.len()).map_err(invalid)?;
|
|
27
|
+
// searchRange and rangeShift are byte counts in 16-bit fields.
|
|
28
|
+
(count > 0 && count <= 4095).ok_or_else(|| invalid("sfnt table count out of range"))?;
|
|
29
|
+
let selector = u16::try_from(count.ilog2()).map_err(invalid)?;
|
|
30
|
+
let range = (1_u16 << selector) * 16;
|
|
31
|
+
let mut data = Vec::with_capacity(font.as_bytes().len());
|
|
32
|
+
data.extend_from_slice(&font.as_bytes()[..4]);
|
|
33
|
+
for value in [count, range, selector, count * 16 - range] {
|
|
34
|
+
data.extend_from_slice(&value.to_be_bytes());
|
|
35
|
+
}
|
|
36
|
+
data.resize(12 + 16 * tags.len(), 0);
|
|
37
|
+
let mut head = None;
|
|
38
|
+
for (index, tag) in tags.into_iter().enumerate() {
|
|
39
|
+
let table = source.table_data(tag).ok_or_else(|| invalid("missing sfnt table"))?;
|
|
40
|
+
let start = data.len();
|
|
41
|
+
let offset = u32::try_from(start).map_err(invalid)?;
|
|
42
|
+
let length = u32::try_from(table.len()).map_err(invalid)?;
|
|
43
|
+
data.extend_from_slice(table.as_bytes());
|
|
44
|
+
if tag == Tag::new(b"head") {
|
|
45
|
+
// head's own checksum is calculated with checkSumAdjustment zero.
|
|
46
|
+
data[start + 8..start + 12].fill(0);
|
|
47
|
+
head = Some(start);
|
|
48
|
+
}
|
|
49
|
+
let sum = checksum(&data[start..]);
|
|
50
|
+
data.resize(data.len().next_multiple_of(4), 0);
|
|
51
|
+
let record = 12 + 16 * index;
|
|
52
|
+
data[record..record + 4].copy_from_slice(&tag.to_be_bytes());
|
|
53
|
+
for (field, value) in [sum, offset, length].into_iter().enumerate() {
|
|
54
|
+
let field = record + 4 * (field + 1);
|
|
55
|
+
data[field..field + 4].copy_from_slice(&value.to_be_bytes());
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
let head = head.ok_or_else(|| invalid("missing head table"))?;
|
|
59
|
+
let adjustment = 0xB1B0_AFBA_u32.wrapping_sub(checksum(&data));
|
|
60
|
+
data[head + 8..head + 12].copy_from_slice(&adjustment.to_be_bytes());
|
|
61
|
+
Ok(data)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
fn checksum(data: &[u8]) -> u32 {
|
|
65
|
+
data.chunks(4).fold(0_u32, |sum, bytes| {
|
|
66
|
+
let mut word = [0; 4];
|
|
67
|
+
word[..bytes.len()].copy_from_slice(bytes);
|
|
68
|
+
sum.wrapping_add(u32::from_be_bytes(word))
|
|
69
|
+
})
|
|
70
|
+
}
|