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.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -0
  3. data/Cargo.lock +153 -3
  4. data/Cargo.toml +1 -1
  5. data/README.md +139 -21
  6. data/ext/imprimatur_native/cli/Cargo.toml +3 -1
  7. data/ext/imprimatur_native/core/Cargo.toml +20 -4
  8. data/ext/imprimatur_native/core/README.md +36 -4
  9. data/ext/imprimatur_native/core/src/archive.rs +3 -16
  10. data/ext/imprimatur_native/core/src/barcode/code128.rs +162 -0
  11. data/ext/imprimatur_native/core/src/barcode/draw.rs +124 -0
  12. data/ext/imprimatur_native/core/src/barcode/ean.rs +91 -0
  13. data/ext/imprimatur_native/core/src/barcode/gs1.rs +188 -0
  14. data/ext/imprimatur_native/core/src/barcode/matrix.rs +130 -0
  15. data/ext/imprimatur_native/core/src/barcode.rs +314 -0
  16. data/ext/imprimatur_native/core/src/doc/paragraph.rs +10 -3
  17. data/ext/imprimatur_native/core/src/doc/picture.rs +192 -0
  18. data/ext/imprimatur_native/core/src/doc/symbol.rs +136 -0
  19. data/ext/imprimatur_native/core/src/doc/table.rs +21 -0
  20. data/ext/imprimatur_native/core/src/doc.rs +96 -2
  21. data/ext/imprimatur_native/core/src/docx/picture.rs +72 -0
  22. data/ext/imprimatur_native/core/src/docx.rs +103 -28
  23. data/ext/imprimatur_native/core/src/error.rs +18 -0
  24. data/ext/imprimatur_native/core/src/font.rs +231 -0
  25. data/ext/imprimatur_native/core/src/lib.rs +6 -1
  26. data/ext/imprimatur_native/core/src/pdf/layout.rs +72 -15
  27. data/ext/imprimatur_native/core/src/pdf/objects.rs +29 -5
  28. data/ext/imprimatur_native/core/src/pdf/picture.rs +197 -0
  29. data/ext/imprimatur_native/core/src/pdf/truetype/descriptor.rs +129 -0
  30. data/ext/imprimatur_native/core/src/pdf/truetype/program.rs +70 -0
  31. data/ext/imprimatur_native/core/src/pdf/truetype.rs +149 -0
  32. data/ext/imprimatur_native/core/src/pdf.rs +40 -4
  33. data/ext/imprimatur_native/core/src/read/content.rs +75 -2
  34. data/ext/imprimatur_native/core/src/read/image.rs +17 -0
  35. data/ext/imprimatur_native/core/src/read/json.rs +94 -7
  36. data/ext/imprimatur_native/core/src/read/numbers.rs +65 -42
  37. data/ext/imprimatur_native/core/src/read/ods.rs +1 -2
  38. data/ext/imprimatur_native/core/src/read/pdf/layout/bidi.rs +212 -0
  39. data/ext/imprimatur_native/core/src/read/pdf/layout.rs +25 -119
  40. data/ext/imprimatur_native/core/src/read/xls.rs +1 -1
  41. data/ext/imprimatur_native/core/src/read/xlsx/formats.rs +155 -0
  42. data/ext/imprimatur_native/core/src/read/xlsx.rs +104 -19
  43. data/ext/imprimatur_native/core/src/read.rs +5 -1
  44. data/ext/imprimatur_native/core/src/script.rs +116 -0
  45. data/ext/imprimatur_native/core/src/style.rs +32 -0
  46. data/ext/imprimatur_native/core/src/xlsx/autofit.rs +60 -0
  47. data/ext/imprimatur_native/core/src/xlsx/date.rs +7 -1
  48. data/ext/imprimatur_native/core/src/xlsx/format.rs +22 -6
  49. data/ext/imprimatur_native/core/src/xlsx/package.rs +22 -8
  50. data/ext/imprimatur_native/core/src/xlsx/styles.rs +9 -8
  51. data/ext/imprimatur_native/core/src/xlsx/worksheet.rs +35 -7
  52. data/ext/imprimatur_native/core/src/xlsx.rs +40 -3
  53. data/ext/imprimatur_native/core/src/xml/reader.rs +10 -0
  54. data/ext/imprimatur_native/ffi/Cargo.toml +3 -2
  55. data/ext/imprimatur_native/ffi/src/archive.rs +1 -3
  56. data/ext/imprimatur_native/ffi/src/barcode.rs +147 -0
  57. data/ext/imprimatur_native/ffi/src/document.rs +76 -1
  58. data/ext/imprimatur_native/ffi/src/lib.rs +40 -1
  59. data/ext/imprimatur_native/ffi/src/read.rs +8 -11
  60. data/ext/imprimatur_native/ffi/src/sheets.rs +114 -0
  61. data/lib/imprimatur/barcode.rb +78 -0
  62. data/lib/imprimatur/document.rb +46 -6
  63. data/lib/imprimatur/reader.rb +2 -2
  64. data/lib/imprimatur/spreadsheet.rb +151 -0
  65. data/lib/imprimatur/version.rb +1 -1
  66. data/lib/imprimatur/xlsx.rb +25 -4
  67. data/lib/imprimatur.rb +19 -3
  68. metadata +23 -2
  69. data/ext/imprimatur_native/core/src/test_support.rs +0 -250
@@ -0,0 +1,130 @@
1
+ //! QR Code and Data Matrix, through the `qrcode` and `datamatrix` crates.
2
+
3
+ use datamatrix::{DataMatrix, SymbolList};
4
+ use qrcode::bits::Bits;
5
+ use qrcode::{Color, EcLevel, QrCode, QrResult, Version};
6
+
7
+ use super::Symbology;
8
+ use super::gs1::Unit;
9
+ use crate::Result;
10
+
11
+ /// How much of a QR Code may be damaged and still read: about 7%, 15%, 25%
12
+ /// or 30% of it.
13
+ #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
14
+ pub enum Level {
15
+ Low,
16
+ #[default]
17
+ Medium,
18
+ Quartile,
19
+ High,
20
+ }
21
+
22
+ impl From<Level> for EcLevel {
23
+ fn from(level: Level) -> Self {
24
+ match level {
25
+ Level::Low => Self::L,
26
+ Level::Medium => Self::M,
27
+ Level::Quartile => Self::Q,
28
+ Level::High => Self::H,
29
+ }
30
+ }
31
+ }
32
+
33
+ /// Where FNC1 separates fields, GS1 symbols carry `GS` (ASCII 29), which
34
+ /// is also what scanners transmit for it.
35
+ const GS: u8 = 0x1D;
36
+ /// Digits shorter than this stay in byte mode: switching to numeric mode
37
+ /// and back costs more than it saves.
38
+ const NUMERIC_RUN: usize = 6;
39
+
40
+ /// A QR Code's side in modules and its modules, row by row.
41
+ pub fn qr(data: &[u8], level: Level) -> Result<(usize, Vec<bool>)> {
42
+ let code = QrCode::with_error_correction_level(data, level.into())
43
+ .map_err(|_| too_long(Symbology::QrCode))?;
44
+ Ok(qr_modules(code))
45
+ }
46
+
47
+ /// A GS1 QR Code (FNC1 in first position) in the smallest version it fits.
48
+ pub fn gs1_qr(units: &[Unit], level: Level) -> Result<(usize, Vec<bool>)> {
49
+ let level = EcLevel::from(level);
50
+ let data = bytes(units);
51
+ let bits = (1..=40)
52
+ .find_map(|version| gs1_bits(&data, Version::Normal(version), level).ok())
53
+ .ok_or_else(|| too_long(Symbology::QrCode))?;
54
+ let code = QrCode::with_bits(bits, level).map_err(|_| too_long(Symbology::QrCode))?;
55
+ Ok(qr_modules(code))
56
+ }
57
+
58
+ /// Long runs of digits in numeric mode, the rest in byte mode: alphanumeric
59
+ /// mode would read a `%` in the data as a separator.
60
+ fn gs1_bits(data: &[u8], version: Version, level: EcLevel) -> QrResult<Bits> {
61
+ let mut bits = Bits::new(version);
62
+ bits.push_fnc1_first_position()?;
63
+ let mut rest = data;
64
+ while !rest.is_empty() {
65
+ let digits = rest.iter().take_while(|byte| byte.is_ascii_digit()).count();
66
+ if digits >= NUMERIC_RUN {
67
+ bits.push_numeric_data(&rest[..digits])?;
68
+ rest = &rest[digits..];
69
+ continue;
70
+ }
71
+ let mut end = 0;
72
+ while end < rest.len() {
73
+ let run = rest[end..].iter().take_while(|byte| byte.is_ascii_digit()).count();
74
+ if run >= NUMERIC_RUN {
75
+ break;
76
+ }
77
+ end += run.max(1);
78
+ }
79
+ bits.push_byte_data(&rest[..end])?;
80
+ rest = &rest[end..];
81
+ }
82
+ bits.push_terminator(level)?;
83
+ Ok(bits)
84
+ }
85
+
86
+ /// A square Data Matrix's columns and rows, and its modules row by row.
87
+ pub fn data_matrix(text: &str) -> Result<((usize, usize), Vec<bool>)> {
88
+ let code =
89
+ DataMatrix::encode_str(text, square()).map_err(|_| too_long(Symbology::DataMatrix))?;
90
+ Ok(modules(&code))
91
+ }
92
+
93
+ /// A square GS1 `DataMatrix`: FNC1 first, then the data.
94
+ pub fn gs1_data_matrix(units: &[Unit]) -> Result<((usize, usize), Vec<bool>)> {
95
+ let code = DataMatrix::encode_gs1(&bytes(units), square())
96
+ .map_err(|_| too_long(Symbology::DataMatrix))?;
97
+ Ok(modules(&code))
98
+ }
99
+
100
+ fn qr_modules(code: QrCode) -> (usize, Vec<bool>) {
101
+ (code.width(), code.into_colors().into_iter().map(|color| color == Color::Dark).collect())
102
+ }
103
+
104
+ fn square() -> SymbolList {
105
+ SymbolList::default().enforce_square()
106
+ }
107
+
108
+ fn modules(code: &DataMatrix) -> ((usize, usize), Vec<bool>) {
109
+ let bitmap = code.bitmap();
110
+ let (columns, rows) = (bitmap.width(), bitmap.height());
111
+ let mut modules = vec![false; columns * rows];
112
+ for (x, y) in bitmap.pixels() {
113
+ modules[y * columns + x] = true;
114
+ }
115
+ ((columns, rows), modules)
116
+ }
117
+
118
+ fn bytes(units: &[Unit]) -> Vec<u8> {
119
+ units
120
+ .iter()
121
+ .map(|unit| match unit {
122
+ Unit::Char(byte) => *byte,
123
+ Unit::Fnc1 => GS,
124
+ })
125
+ .collect()
126
+ }
127
+
128
+ fn too_long(symbology: Symbology) -> crate::Error {
129
+ symbology.invalid("too much data for the largest symbol")
130
+ }
@@ -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
+ }
@@ -46,8 +46,9 @@ impl Run {
46
46
  }
47
47
  }
48
48
 
49
- /// Runs of text laid out together. Left aligned unless told otherwise, or
50
- /// right aligned when right to left.
49
+ /// Runs of text laid out together, in the direction set or else that of
50
+ /// their first letter. Left aligned unless told otherwise, or right aligned
51
+ /// when right to left.
51
52
  #[derive(Debug, Clone, Default, PartialEq, Eq)]
52
53
  pub struct Paragraph {
53
54
  pub(crate) runs: Vec<Run>,
@@ -102,9 +103,15 @@ impl Paragraph {
102
103
  &self.runs
103
104
  }
104
105
 
106
+ /// The direction the paragraph reads in: as set, or else that of its
107
+ /// first letter.
108
+ pub(crate) fn effective_direction(&self) -> Option<TextDirection> {
109
+ self.direction.or_else(|| self.runs.iter().find_map(|run| TextDirection::of(&run.text)))
110
+ }
111
+
105
112
  /// The alignment the paragraph is laid out with.
106
113
  pub(crate) fn effective_align(&self) -> Align {
107
- self.align.unwrap_or(match self.direction {
114
+ self.align.unwrap_or_else(|| match self.effective_direction() {
108
115
  Some(TextDirection::RightToLeft) => Align::Right,
109
116
  _ => Align::Left,
110
117
  })
@@ -0,0 +1,192 @@
1
+ //! Pictures: PNG or JPEG files, read once for what PDF and DOCX need.
2
+
3
+ use std::sync::Arc;
4
+
5
+ use crate::style::Align;
6
+ use crate::{Error, Result};
7
+
8
+ /// Pixels per point at 96 pixels an inch: a picture's size when none is set.
9
+ const POINTS_PER_PIXEL: f64 = 0.75;
10
+ /// The largest side, in points, a picture may be set to: PDF's page limit.
11
+ const MAX_POINTS: f64 = 14_400.0;
12
+ /// The largest side, in pixels, a picture may have.
13
+ const MAX_PIXELS: u32 = 1 << 16;
14
+
15
+ /// A PNG or JPEG drawn in the document's flow.
16
+ ///
17
+ /// It is drawn at the width or height set (in points, the other following
18
+ /// its proportions), or else at 96 pixels an inch, never larger than the
19
+ /// page leaves room for.
20
+ ///
21
+ /// ```no_run
22
+ /// use imprimatur::doc::{Document, Picture};
23
+ /// use imprimatur::style::Align;
24
+ ///
25
+ /// let mut doc = Document::new();
26
+ /// doc.picture(Picture::new(std::fs::read("logo.png")?)?.width(144.0)?.align(Align::Center));
27
+ /// # Ok::<(), imprimatur::Error>(())
28
+ /// ```
29
+ #[derive(Debug, Clone, PartialEq)]
30
+ pub struct Picture {
31
+ pub(crate) data: Arc<[u8]>,
32
+ pub(crate) kind: Kind,
33
+ pub(crate) pixels: (u32, u32),
34
+ width: Option<f64>,
35
+ height: Option<f64>,
36
+ pub(crate) align: Option<Align>,
37
+ }
38
+
39
+ /// The format, and what writing it takes.
40
+ #[derive(Debug, Clone, PartialEq, Eq)]
41
+ pub enum Kind {
42
+ Png(Png),
43
+ /// Gray (1), RGB (3) or CMYK (4) components; Adobe's CMYK is inverted.
44
+ Jpeg {
45
+ components: u8,
46
+ inverted: bool,
47
+ },
48
+ }
49
+
50
+ /// A PNG's pixel layout and its compressed pixels (its `IDAT` chunks).
51
+ #[derive(Debug, Clone, PartialEq, Eq)]
52
+ pub struct Png {
53
+ pub depth: u8,
54
+ /// 0 gray, 2 RGB, 3 palette, 4 gray and alpha, 6 RGB and alpha.
55
+ pub color: u8,
56
+ pub palette: Vec<u8>,
57
+ /// The `tRNS` chunk: a palette's alphas, or the transparent color.
58
+ pub transparency: Vec<u8>,
59
+ pub compressed: Vec<u8>,
60
+ }
61
+
62
+ impl Picture {
63
+ /// # Errors
64
+ /// [`Error::InvalidImage`] for data that is no PNG or JPEG, a damaged
65
+ /// one, an interlaced PNG, a JPEG other than 8 bits a sample, or one
66
+ /// larger than 65 536 pixels a side.
67
+ pub fn new(data: impl Into<Vec<u8>>) -> Result<Self> {
68
+ let data: Arc<[u8]> = data.into().into();
69
+ let (kind, pixels) = if data.starts_with(b"\x89PNG\r\n\x1a\n") {
70
+ png(&data)?
71
+ } else if data.starts_with(&[0xFF, 0xD8, 0xFF]) {
72
+ jpeg(&data)?
73
+ } else {
74
+ return Err(Error::InvalidImage("not a PNG or JPEG image"));
75
+ };
76
+ let valid = (1..=MAX_PIXELS).contains(&pixels.0) && (1..=MAX_PIXELS).contains(&pixels.1);
77
+ valid.ok_or(Error::InvalidImage("a picture of 1 to 65 536 pixels a side"))?;
78
+ Ok(Self { data, kind, pixels, width: None, height: None, align: None })
79
+ }
80
+
81
+ /// Its width in points; the height follows unless set too.
82
+ ///
83
+ /// # Errors
84
+ /// [`Error::InvalidImage`] outside 1 to 14 400 points.
85
+ pub fn width(mut self, points: f64) -> Result<Self> {
86
+ self.width = Some(points_in_range(points)?);
87
+ Ok(self)
88
+ }
89
+
90
+ /// Its height in points; the width follows unless set too.
91
+ ///
92
+ /// # Errors
93
+ /// [`Error::InvalidImage`] outside 1 to 14 400 points.
94
+ pub fn height(mut self, points: f64) -> Result<Self> {
95
+ self.height = Some(points_in_range(points)?);
96
+ Ok(self)
97
+ }
98
+
99
+ #[must_use]
100
+ pub const fn align(mut self, align: Align) -> Self {
101
+ self.align = Some(align);
102
+ self
103
+ }
104
+
105
+ /// The size it is drawn at, in points, fitted into `room` (the width
106
+ /// and height between the margins).
107
+ pub(crate) fn size(&self, room: (f64, f64)) -> (f64, f64) {
108
+ let (pixels_wide, pixels_high) = (f64::from(self.pixels.0), f64::from(self.pixels.1));
109
+ let (width, height) = match (self.width, self.height) {
110
+ (Some(width), Some(height)) => (width, height),
111
+ (Some(width), None) => (width, width * pixels_high / pixels_wide),
112
+ (None, Some(height)) => (height * pixels_wide / pixels_high, height),
113
+ (None, None) => (pixels_wide * POINTS_PER_PIXEL, pixels_high * POINTS_PER_PIXEL),
114
+ };
115
+ let scale = (room.0 / width).min(room.1 / height).min(1.0);
116
+ (width * scale, height * scale)
117
+ }
118
+ }
119
+
120
+ fn points_in_range(points: f64) -> Result<f64> {
121
+ (1.0..=MAX_POINTS)
122
+ .contains(&points)
123
+ .then_some(points)
124
+ .ok_or(Error::InvalidImage("a picture of 1 to 14 400 points a side"))
125
+ }
126
+
127
+ /// A PNG's chunks; interlaced ones are refused.
128
+ fn png(data: &[u8]) -> Result<(Kind, (u32, u32))> {
129
+ let damaged = || Error::InvalidImage("a damaged PNG");
130
+ let mut png = Png {
131
+ depth: 0,
132
+ color: 0,
133
+ palette: Vec::new(),
134
+ transparency: Vec::new(),
135
+ compressed: Vec::new(),
136
+ };
137
+ let (mut pixels, mut at) = (None, 8);
138
+ while let Some(head) = data.get(at..at + 8) {
139
+ let length = u32::from_be_bytes([head[0], head[1], head[2], head[3]]) as usize;
140
+ let body = data.get(at + 8..at + 8 + length).ok_or_else(damaged)?;
141
+ match &head[4..8] {
142
+ b"IHDR" if body.len() == 13 => {
143
+ let side = |at: usize| {
144
+ u32::from_be_bytes([body[at], body[at + 1], body[at + 2], body[at + 3]])
145
+ };
146
+ pixels = Some((side(0), side(4)));
147
+ (png.depth, png.color) = (body[8], body[9]);
148
+ (body[12] == 0).ok_or(Error::InvalidImage("an interlaced PNG"))?;
149
+ }
150
+ b"PLTE" => png.palette = body.to_vec(),
151
+ b"tRNS" => png.transparency = body.to_vec(),
152
+ b"IDAT" => png.compressed.extend_from_slice(body),
153
+ b"IEND" => break,
154
+ _ => {}
155
+ }
156
+ at += 12 + length;
157
+ }
158
+ let layout = matches!(
159
+ (png.color, png.depth),
160
+ (0, 1 | 2 | 4 | 8 | 16) | (3, 1 | 2 | 4 | 8) | (2 | 4 | 6, 8 | 16)
161
+ );
162
+ let palette = png.color != 3 || !png.palette.is_empty();
163
+ let pixels = pixels.filter(|_| layout && palette && !png.compressed.is_empty());
164
+ Ok((Kind::Png(png), pixels.ok_or_else(damaged)?))
165
+ }
166
+
167
+ /// A JPEG's frame header (`SOF`), and Adobe's mark, which inverts CMYK.
168
+ fn jpeg(data: &[u8]) -> Result<(Kind, (u32, u32))> {
169
+ let damaged = || Error::InvalidImage("a damaged JPEG");
170
+ let (mut at, mut inverted) = (2, false);
171
+ loop {
172
+ let &[0xFF, marker, high, low] = data.get(at..at + 4).ok_or_else(damaged)? else {
173
+ return Err(damaged());
174
+ };
175
+ let length = usize::from(u16::from_be_bytes([high, low]));
176
+ if marker == 0xEE && data.get(at + 4..at + 9) == Some(b"Adobe") {
177
+ inverted = true;
178
+ }
179
+ // SOF0 to SOF15, but DHT, JPG and DAC share the range.
180
+ if (0xC0..=0xCF).contains(&marker) && !matches!(marker, 0xC4 | 0xC8 | 0xCC) {
181
+ let frame = data.get(at + 4..at + 10).ok_or_else(damaged)?;
182
+ (frame[0] == 8).ok_or(Error::InvalidImage("a JPEG of 8 bits a sample"))?;
183
+ let height = u32::from(u16::from_be_bytes([frame[1], frame[2]]));
184
+ let width = u32::from(u16::from_be_bytes([frame[3], frame[4]]));
185
+ let components = frame[5];
186
+ matches!(components, 1 | 3 | 4).ok_or_else(damaged)?;
187
+ let kind = Kind::Jpeg { components, inverted: inverted && components == 4 };
188
+ return Ok((kind, (width, height)));
189
+ }
190
+ at += 2 + length;
191
+ }
192
+ }