imprimatur 0.3.0 → 0.5.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 +16 -0
- data/Cargo.lock +33 -3
- data/Cargo.toml +1 -1
- data/README.md +48 -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/reader.rb +48 -11
- data/lib/imprimatur/spreadsheet.rb +1 -1
- data/lib/imprimatur/version.rb +1 -1
- data/lib/imprimatur.rb +6 -1
- metadata +13 -2
- data/ext/imprimatur_native/core/src/test_support.rs +0 -251
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
//! Barcodes: QR Code and Data Matrix, EAN-13, EAN-8, UPC-A and Code 128,
|
|
2
|
+
//! and the GS1 forms of QR Code, Data Matrix and Code 128 (GS1-128), drawn
|
|
3
|
+
//! as SVG, as PNG, or into a [`Document`](crate::doc::Document) as shapes.
|
|
4
|
+
//!
|
|
5
|
+
//! QR Code and Data Matrix come from the `qrcode` and `datamatrix` crates;
|
|
6
|
+
//! the linear symbologies, GS1 element strings and the drawing are ours.
|
|
7
|
+
//!
|
|
8
|
+
//! ```
|
|
9
|
+
//! use imprimatur::barcode::{Barcode, Level, Look};
|
|
10
|
+
//!
|
|
11
|
+
//! let qr = Barcode::qr("https://id.gs1.org/01/09506000134352", Level::Medium)?;
|
|
12
|
+
//! assert!(qr.svg(4.0, &Look::default())?.starts_with("<svg"));
|
|
13
|
+
//!
|
|
14
|
+
//! let ean = Barcode::ean13("950600013435")?;
|
|
15
|
+
//! assert_eq!(ean.text(), "9506000134352"); // the check digit, added
|
|
16
|
+
//!
|
|
17
|
+
//! let label = Barcode::gs1_128("(00)106141412345678908")?;
|
|
18
|
+
//! assert!(label.png(3, &Look::default())?.starts_with(b"\x89PNG"));
|
|
19
|
+
//! # Ok::<(), imprimatur::Error>(())
|
|
20
|
+
//! ```
|
|
21
|
+
|
|
22
|
+
mod code128;
|
|
23
|
+
mod draw;
|
|
24
|
+
mod ean;
|
|
25
|
+
mod gs1;
|
|
26
|
+
mod matrix;
|
|
27
|
+
#[cfg(test)]
|
|
28
|
+
mod tests;
|
|
29
|
+
|
|
30
|
+
pub(crate) use draw::Shapes;
|
|
31
|
+
pub use gs1::check_digit;
|
|
32
|
+
pub use matrix::Level;
|
|
33
|
+
|
|
34
|
+
use crate::style::Color;
|
|
35
|
+
use crate::{Error, Result};
|
|
36
|
+
|
|
37
|
+
/// The kinds of barcode.
|
|
38
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
39
|
+
#[non_exhaustive]
|
|
40
|
+
pub enum Symbology {
|
|
41
|
+
QrCode,
|
|
42
|
+
DataMatrix,
|
|
43
|
+
Ean13,
|
|
44
|
+
Ean8,
|
|
45
|
+
UpcA,
|
|
46
|
+
Code128,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
impl Symbology {
|
|
50
|
+
#[must_use]
|
|
51
|
+
pub const fn name(self) -> &'static str {
|
|
52
|
+
match self {
|
|
53
|
+
Self::QrCode => "QR Code",
|
|
54
|
+
Self::DataMatrix => "Data Matrix",
|
|
55
|
+
Self::Ean13 => "EAN-13",
|
|
56
|
+
Self::Ean8 => "EAN-8",
|
|
57
|
+
Self::UpcA => "UPC-A",
|
|
58
|
+
Self::Code128 => "Code 128",
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// Whether it is one row of bars, read across.
|
|
63
|
+
#[must_use]
|
|
64
|
+
pub const fn is_linear(self) -> bool {
|
|
65
|
+
matches!(self, Self::Ean13 | Self::Ean8 | Self::UpcA | Self::Code128)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/// The blank margin readers need, in modules: left, right, and above
|
|
69
|
+
/// and below.
|
|
70
|
+
const fn quiet_zone(self) -> (usize, usize, usize) {
|
|
71
|
+
match self {
|
|
72
|
+
Self::QrCode => (4, 4, 4),
|
|
73
|
+
Self::DataMatrix => (1, 1, 1),
|
|
74
|
+
Self::Ean13 => (11, 7, 0),
|
|
75
|
+
Self::Ean8 => (7, 7, 0),
|
|
76
|
+
Self::UpcA => (9, 9, 0),
|
|
77
|
+
Self::Code128 => (10, 10, 0),
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
pub(crate) fn invalid(self, reason: impl Into<String>) -> Error {
|
|
82
|
+
Error::InvalidBarcode { symbology: self.name(), reason: reason.into() }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/// A barcode symbol: its modules, dark or light, ready to draw.
|
|
87
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
88
|
+
pub struct Barcode {
|
|
89
|
+
symbology: Symbology,
|
|
90
|
+
gs1: bool,
|
|
91
|
+
text: String,
|
|
92
|
+
columns: usize,
|
|
93
|
+
rows: usize,
|
|
94
|
+
/// Row by row; a linear code has one row.
|
|
95
|
+
modules: Vec<bool>,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
impl Barcode {
|
|
99
|
+
/// A QR Code of text, such as a URL (a GS1 Digital Link is one).
|
|
100
|
+
///
|
|
101
|
+
/// # Errors
|
|
102
|
+
/// [`Error::InvalidBarcode`] when the text does not fit at `level`.
|
|
103
|
+
pub fn qr(text: &str, level: Level) -> Result<Self> {
|
|
104
|
+
let (side, modules) = matrix::qr(text.as_bytes(), level)?;
|
|
105
|
+
Ok(Self::matrix(Symbology::QrCode, false, text, (side, side), modules))
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/// A GS1 QR Code of an element string such as
|
|
109
|
+
/// `(01)09506000134352(10)AB12`.
|
|
110
|
+
///
|
|
111
|
+
/// # Errors
|
|
112
|
+
/// [`Error::InvalidElementString`], or [`Error::InvalidBarcode`] when it
|
|
113
|
+
/// does not fit at `level`.
|
|
114
|
+
pub fn gs1_qr(element_string: &str, level: Level) -> Result<Self> {
|
|
115
|
+
let units = gs1::element_units(element_string)?;
|
|
116
|
+
let (side, modules) = matrix::gs1_qr(&units, level)?;
|
|
117
|
+
Ok(Self::matrix(Symbology::QrCode, true, element_string, (side, side), modules))
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/// A square Data Matrix of text: Latin-1 as is, anything else as UTF-8.
|
|
121
|
+
///
|
|
122
|
+
/// # Errors
|
|
123
|
+
/// [`Error::InvalidBarcode`] when the text does not fit.
|
|
124
|
+
pub fn data_matrix(text: &str) -> Result<Self> {
|
|
125
|
+
let (size, modules) = matrix::data_matrix(text)?;
|
|
126
|
+
Ok(Self::matrix(Symbology::DataMatrix, false, text, size, modules))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/// A square GS1 `DataMatrix` of an element string such as
|
|
130
|
+
/// `(01)09506000134352(17)251231(10)AB12`.
|
|
131
|
+
///
|
|
132
|
+
/// # Errors
|
|
133
|
+
/// [`Error::InvalidElementString`], or [`Error::InvalidBarcode`] when it
|
|
134
|
+
/// does not fit.
|
|
135
|
+
pub fn gs1_data_matrix(element_string: &str) -> Result<Self> {
|
|
136
|
+
let units = gs1::element_units(element_string)?;
|
|
137
|
+
let (size, modules) = matrix::gs1_data_matrix(&units)?;
|
|
138
|
+
Ok(Self::matrix(Symbology::DataMatrix, true, element_string, size, modules))
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/// An EAN-13 of a GTIN-13: 12 digits and the check digit is added, or
|
|
142
|
+
/// 13 and it is checked.
|
|
143
|
+
///
|
|
144
|
+
/// # Errors
|
|
145
|
+
/// [`Error::InvalidBarcode`] for anything else, or a wrong check digit.
|
|
146
|
+
pub fn ean13(digits: &str) -> Result<Self> {
|
|
147
|
+
let digits = ean::digits(Symbology::Ean13, digits, 13)?;
|
|
148
|
+
Ok(Self::linear(Symbology::Ean13, false, ean::text(&digits), ean::ean13(&digits)))
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/// An EAN-8 of a GTIN-8: 7 digits, or 8 with the check digit.
|
|
152
|
+
///
|
|
153
|
+
/// # Errors
|
|
154
|
+
/// [`Error::InvalidBarcode`] for anything else, or a wrong check digit.
|
|
155
|
+
pub fn ean8(digits: &str) -> Result<Self> {
|
|
156
|
+
let digits = ean::digits(Symbology::Ean8, digits, 8)?;
|
|
157
|
+
Ok(Self::linear(Symbology::Ean8, false, ean::text(&digits), ean::ean8(&digits)))
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/// A UPC-A of a GTIN-12: 11 digits, or 12 with the check digit. Its bars
|
|
161
|
+
/// are those of the EAN-13 with a leading zero.
|
|
162
|
+
///
|
|
163
|
+
/// # Errors
|
|
164
|
+
/// [`Error::InvalidBarcode`] for anything else, or a wrong check digit.
|
|
165
|
+
pub fn upc_a(digits: &str) -> Result<Self> {
|
|
166
|
+
let digits = ean::digits(Symbology::UpcA, digits, 12)?;
|
|
167
|
+
let mut ean = Vec::with_capacity(13);
|
|
168
|
+
ean.push(0);
|
|
169
|
+
ean.extend_from_slice(&digits);
|
|
170
|
+
Ok(Self::linear(Symbology::UpcA, false, ean::text(&digits), ean::ean13(&ean)))
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/// A Code 128 of ASCII text, switching to code set C for runs of digits.
|
|
174
|
+
///
|
|
175
|
+
/// # Errors
|
|
176
|
+
/// [`Error::InvalidBarcode`] for text beyond ASCII, empty, or longer than
|
|
177
|
+
/// 128 characters.
|
|
178
|
+
pub fn code128(text: &str) -> Result<Self> {
|
|
179
|
+
let units: Vec<gs1::Unit> = text.bytes().map(gs1::Unit::Char).collect();
|
|
180
|
+
(!units.is_empty()).ok_or_else(|| Symbology::Code128.invalid("no text"))?;
|
|
181
|
+
(units.len() <= 128).ok_or_else(|| Symbology::Code128.invalid("128 characters at most"))?;
|
|
182
|
+
Ok(Self::linear(Symbology::Code128, false, text.to_owned(), code128::encode(&units)?))
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/// A GS1-128 of an element string such as `(00)106141412345678908`:
|
|
186
|
+
/// Code 128 opening with FNC1, at most 48 characters with separators.
|
|
187
|
+
///
|
|
188
|
+
/// # Errors
|
|
189
|
+
/// [`Error::InvalidElementString`], or [`Error::InvalidBarcode`] when it
|
|
190
|
+
/// is too long.
|
|
191
|
+
pub fn gs1_128(element_string: &str) -> Result<Self> {
|
|
192
|
+
let mut units = vec![gs1::Unit::Fnc1];
|
|
193
|
+
units.extend(gs1::element_units(element_string)?);
|
|
194
|
+
let characters = units.len() - 1;
|
|
195
|
+
(characters <= 48).ok_or_else(|| {
|
|
196
|
+
Symbology::Code128
|
|
197
|
+
.invalid(format!("GS1-128 holds 48 characters with separators, not {characters}"))
|
|
198
|
+
})?;
|
|
199
|
+
let modules = code128::encode(&units)?;
|
|
200
|
+
Ok(Self::linear(Symbology::Code128, true, element_string.to_owned(), modules))
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
fn matrix(
|
|
204
|
+
symbology: Symbology,
|
|
205
|
+
gs1: bool,
|
|
206
|
+
text: &str,
|
|
207
|
+
(columns, rows): (usize, usize),
|
|
208
|
+
modules: Vec<bool>,
|
|
209
|
+
) -> Self {
|
|
210
|
+
Self { symbology, gs1, text: text.to_owned(), columns, rows, modules }
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const fn linear(symbology: Symbology, gs1: bool, text: String, modules: Vec<bool>) -> Self {
|
|
214
|
+
Self { symbology, gs1, text, columns: modules.len(), rows: 1, modules }
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
#[must_use]
|
|
218
|
+
pub const fn symbology(&self) -> Symbology {
|
|
219
|
+
self.symbology
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/// Whether it carries a GS1 element string (its FNC1 mode).
|
|
223
|
+
#[must_use]
|
|
224
|
+
pub const fn is_gs1(&self) -> bool {
|
|
225
|
+
self.gs1
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/// What it encodes: the text or element string given, or a GTIN's
|
|
229
|
+
/// digits with the check digit.
|
|
230
|
+
#[must_use]
|
|
231
|
+
pub fn text(&self) -> &str {
|
|
232
|
+
&self.text
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/// Columns and rows of modules, without the quiet zone; a linear code
|
|
236
|
+
/// is one row.
|
|
237
|
+
#[must_use]
|
|
238
|
+
pub const fn size(&self) -> (usize, usize) {
|
|
239
|
+
(self.columns, self.rows)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/// Whether the module at column `x`, row `y` is dark; false outside.
|
|
243
|
+
#[must_use]
|
|
244
|
+
pub fn is_dark(&self, x: usize, y: usize) -> bool {
|
|
245
|
+
x < self.columns && self.modules.get(y * self.columns + x).copied().unwrap_or(false)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/// SVG markup, `module` user units (pixels) a module.
|
|
249
|
+
///
|
|
250
|
+
/// # Errors
|
|
251
|
+
/// [`Error::InvalidBarcode`] for a module outside 0.01 to 1000.
|
|
252
|
+
pub fn svg(&self, module: f64, look: &Look) -> Result<String> {
|
|
253
|
+
(0.01..=1000.0)
|
|
254
|
+
.contains(&module)
|
|
255
|
+
.ok_or_else(|| self.symbology.invalid("a module of 0.01 to 1000 units"))?;
|
|
256
|
+
Ok(draw::svg(&self.shapes(look.quiet_zone, look.bar_height), module, look))
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/// A PNG, `scale` pixels a module.
|
|
260
|
+
///
|
|
261
|
+
/// # Errors
|
|
262
|
+
/// [`Error::InvalidBarcode`] for a scale of 0, or an image above 65 536
|
|
263
|
+
/// pixels a side.
|
|
264
|
+
pub fn png(&self, scale: u32, look: &Look) -> Result<Vec<u8>> {
|
|
265
|
+
draw::png(&self.shapes(look.quiet_zone, look.bar_height), scale, look)
|
|
266
|
+
.ok_or_else(|| self.symbology.invalid("a scale of 1 or more, under 65 536 pixels"))
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/// A module's size in points when a document is not told: 0.33 mm for
|
|
270
|
+
/// retail EAN and UPC, 0.495 mm for GS1-128 (logistics labels), 0.35 mm
|
|
271
|
+
/// for other Code 128, 0.7 mm for QR Code and Data Matrix.
|
|
272
|
+
pub(crate) const fn default_module(&self) -> f64 {
|
|
273
|
+
match self.symbology {
|
|
274
|
+
Symbology::Ean13 | Symbology::Ean8 | Symbology::UpcA => 0.935,
|
|
275
|
+
Symbology::Code128 if self.gs1 => 1.4,
|
|
276
|
+
Symbology::Code128 => 1.0,
|
|
277
|
+
Symbology::QrCode | Symbology::DataMatrix => 2.0,
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/// A linear code's bar height in modules when not told: GS1's nominal
|
|
282
|
+
/// 22.85 mm (EAN-13, UPC-A) and 18.23 mm (EAN-8) at 0.33 mm, and for
|
|
283
|
+
/// Code 128 ISO/IEC 15417's 15% of its width, 20 modules at least.
|
|
284
|
+
const fn default_bar_height(&self) -> usize {
|
|
285
|
+
match self.symbology {
|
|
286
|
+
Symbology::Ean13 | Symbology::UpcA => 69,
|
|
287
|
+
Symbology::Ean8 => 55,
|
|
288
|
+
_ => {
|
|
289
|
+
let height = (self.columns * 15).div_ceil(100);
|
|
290
|
+
if height > 20 { height } else { 20 }
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/// How a barcode is drawn as SVG or PNG.
|
|
297
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
298
|
+
pub struct Look {
|
|
299
|
+
pub dark: Color,
|
|
300
|
+
/// The background; `None` leaves it transparent.
|
|
301
|
+
pub light: Option<Color>,
|
|
302
|
+
/// Keep the blank margin the symbology needs around it.
|
|
303
|
+
pub quiet_zone: bool,
|
|
304
|
+
/// A linear code's bar height in modules; `None` takes the
|
|
305
|
+
/// symbology's nominal height.
|
|
306
|
+
pub bar_height: Option<u32>,
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
impl Default for Look {
|
|
310
|
+
/// Black on white, with its quiet zone.
|
|
311
|
+
fn default() -> Self {
|
|
312
|
+
Self { dark: Color::BLACK, light: Some(Color::WHITE), quiet_zone: true, bar_height: None }
|
|
313
|
+
}
|
|
314
|
+
}
|
|
@@ -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;
|