imprimatur 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +31 -0
- data/Cargo.lock +153 -3
- data/Cargo.toml +1 -1
- data/README.md +139 -21
- data/ext/imprimatur_native/cli/Cargo.toml +3 -1
- data/ext/imprimatur_native/core/Cargo.toml +20 -4
- data/ext/imprimatur_native/core/README.md +36 -4
- data/ext/imprimatur_native/core/src/archive.rs +3 -16
- data/ext/imprimatur_native/core/src/barcode/code128.rs +162 -0
- data/ext/imprimatur_native/core/src/barcode/draw.rs +124 -0
- data/ext/imprimatur_native/core/src/barcode/ean.rs +91 -0
- data/ext/imprimatur_native/core/src/barcode/gs1.rs +188 -0
- data/ext/imprimatur_native/core/src/barcode/matrix.rs +130 -0
- data/ext/imprimatur_native/core/src/barcode.rs +314 -0
- data/ext/imprimatur_native/core/src/doc/paragraph.rs +10 -3
- data/ext/imprimatur_native/core/src/doc/picture.rs +192 -0
- data/ext/imprimatur_native/core/src/doc/symbol.rs +136 -0
- data/ext/imprimatur_native/core/src/doc/table.rs +21 -0
- data/ext/imprimatur_native/core/src/doc.rs +96 -2
- data/ext/imprimatur_native/core/src/docx/picture.rs +72 -0
- data/ext/imprimatur_native/core/src/docx.rs +103 -28
- data/ext/imprimatur_native/core/src/error.rs +18 -0
- data/ext/imprimatur_native/core/src/font.rs +231 -0
- data/ext/imprimatur_native/core/src/lib.rs +6 -1
- data/ext/imprimatur_native/core/src/pdf/layout.rs +72 -15
- data/ext/imprimatur_native/core/src/pdf/objects.rs +29 -5
- data/ext/imprimatur_native/core/src/pdf/picture.rs +197 -0
- data/ext/imprimatur_native/core/src/pdf/truetype/descriptor.rs +129 -0
- data/ext/imprimatur_native/core/src/pdf/truetype/program.rs +70 -0
- data/ext/imprimatur_native/core/src/pdf/truetype.rs +149 -0
- data/ext/imprimatur_native/core/src/pdf.rs +40 -4
- data/ext/imprimatur_native/core/src/read/content.rs +75 -2
- data/ext/imprimatur_native/core/src/read/image.rs +17 -0
- data/ext/imprimatur_native/core/src/read/json.rs +94 -7
- data/ext/imprimatur_native/core/src/read/numbers.rs +65 -42
- data/ext/imprimatur_native/core/src/read/ods.rs +1 -2
- data/ext/imprimatur_native/core/src/read/pdf/layout/bidi.rs +212 -0
- data/ext/imprimatur_native/core/src/read/pdf/layout.rs +25 -119
- data/ext/imprimatur_native/core/src/read/xls.rs +1 -1
- data/ext/imprimatur_native/core/src/read/xlsx/formats.rs +155 -0
- data/ext/imprimatur_native/core/src/read/xlsx.rs +104 -19
- data/ext/imprimatur_native/core/src/read.rs +5 -1
- data/ext/imprimatur_native/core/src/script.rs +116 -0
- data/ext/imprimatur_native/core/src/style.rs +32 -0
- data/ext/imprimatur_native/core/src/xlsx/autofit.rs +60 -0
- data/ext/imprimatur_native/core/src/xlsx/date.rs +7 -1
- data/ext/imprimatur_native/core/src/xlsx/format.rs +22 -6
- data/ext/imprimatur_native/core/src/xlsx/package.rs +22 -8
- data/ext/imprimatur_native/core/src/xlsx/styles.rs +9 -8
- data/ext/imprimatur_native/core/src/xlsx/worksheet.rs +35 -7
- data/ext/imprimatur_native/core/src/xlsx.rs +40 -3
- data/ext/imprimatur_native/core/src/xml/reader.rs +10 -0
- data/ext/imprimatur_native/ffi/Cargo.toml +3 -2
- data/ext/imprimatur_native/ffi/src/archive.rs +1 -3
- data/ext/imprimatur_native/ffi/src/barcode.rs +147 -0
- data/ext/imprimatur_native/ffi/src/document.rs +76 -1
- data/ext/imprimatur_native/ffi/src/lib.rs +40 -1
- data/ext/imprimatur_native/ffi/src/read.rs +8 -11
- data/ext/imprimatur_native/ffi/src/sheets.rs +114 -0
- data/lib/imprimatur/barcode.rb +78 -0
- data/lib/imprimatur/document.rb +46 -6
- data/lib/imprimatur/reader.rb +2 -2
- data/lib/imprimatur/spreadsheet.rb +151 -0
- data/lib/imprimatur/version.rb +1 -1
- data/lib/imprimatur/xlsx.rb +25 -4
- data/lib/imprimatur.rb +19 -3
- metadata +23 -2
- data/ext/imprimatur_native/core/src/test_support.rs +0 -250
|
@@ -32,6 +32,7 @@ use ::zip::result::ZipError;
|
|
|
32
32
|
use ::zip::write::FullFileOptions;
|
|
33
33
|
use ::zip::{CompressionMethod, DateTime, ZipArchive, ZipWriter};
|
|
34
34
|
|
|
35
|
+
use crate::xlsx::civil_from_days;
|
|
35
36
|
use crate::{Error, Result};
|
|
36
37
|
|
|
37
38
|
/// An archive being written; [`finish`](Self::finish) writes its directory.
|
|
@@ -129,9 +130,10 @@ fn dos_time(time: Option<SystemTime>) -> DateTime {
|
|
|
129
130
|
return DateTime::default();
|
|
130
131
|
};
|
|
131
132
|
let days = i64::try_from(seconds / 86_400).unwrap_or(i64::MAX);
|
|
132
|
-
let (year, month, day) =
|
|
133
|
+
let (year, month, day) = civil_from_days(days);
|
|
133
134
|
let clock = seconds % 86_400;
|
|
134
135
|
let part = |value: u64| u8::try_from(value).unwrap_or(0);
|
|
136
|
+
let (month, day) = (u8::try_from(month).unwrap_or(1), u8::try_from(day).unwrap_or(1));
|
|
135
137
|
u16::try_from(year)
|
|
136
138
|
.ok()
|
|
137
139
|
.and_then(|year| {
|
|
@@ -148,21 +150,6 @@ fn dos_time(time: Option<SystemTime>) -> DateTime {
|
|
|
148
150
|
.unwrap_or_default()
|
|
149
151
|
}
|
|
150
152
|
|
|
151
|
-
/// Days since 1970-01-01 as a proleptic Gregorian date (Howard Hinnant's
|
|
152
|
-
/// `civil_from_days`).
|
|
153
|
-
fn civil(days: i64) -> (i64, u8, u8) {
|
|
154
|
-
let days = days + 719_468;
|
|
155
|
-
let era = days.div_euclid(146_097);
|
|
156
|
-
let of_era = days.rem_euclid(146_097);
|
|
157
|
-
let year_of_era = (of_era - of_era / 1460 + of_era / 36_524 - of_era / 146_096) / 365;
|
|
158
|
-
let of_year = of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
|
|
159
|
-
let shifted = (5 * of_year + 2) / 153;
|
|
160
|
-
let day = u8::try_from(of_year - (153 * shifted + 2) / 5 + 1).unwrap_or(1);
|
|
161
|
-
let month = u8::try_from(if shifted < 10 { shifted + 3 } else { shifted - 9 }).unwrap_or(1);
|
|
162
|
-
let year = year_of_era + era * 400 + i64::from(month <= 2);
|
|
163
|
-
(year, month, day)
|
|
164
|
-
}
|
|
165
|
-
|
|
166
153
|
/// What the archive's directory says of an entry.
|
|
167
154
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
168
155
|
pub struct Entry {
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
//! Code 128 (ISO/IEC 15417): ASCII in code sets A (controls, upper case)
|
|
2
|
+
//! and B (upper and lower case), and pairs of digits in set C, switching
|
|
3
|
+
//! as Annex E says; FNC1 first makes it GS1-128.
|
|
4
|
+
|
|
5
|
+
use super::Symbology;
|
|
6
|
+
use super::gs1::Unit;
|
|
7
|
+
use crate::Result;
|
|
8
|
+
|
|
9
|
+
/// Each symbol's bar and space widths, a bar first: values 0 to 102, then
|
|
10
|
+
/// Start A, B and C (103 to 105).
|
|
11
|
+
const PATTERNS: [&[u8; 6]; 106] = [
|
|
12
|
+
b"212222", b"222122", b"222221", b"121223", b"121322", b"131222", b"122213", b"122312",
|
|
13
|
+
b"132212", b"221213", b"221312", b"231212", b"112232", b"122132", b"122231", b"113222",
|
|
14
|
+
b"123122", b"123221", b"223211", b"221132", b"221231", b"213212", b"223112", b"312131",
|
|
15
|
+
b"311222", b"321122", b"321221", b"312212", b"322112", b"322211", b"212123", b"212321",
|
|
16
|
+
b"232121", b"111323", b"131123", b"131321", b"112313", b"132113", b"132311", b"211313",
|
|
17
|
+
b"231113", b"231311", b"112133", b"112331", b"132131", b"113123", b"113321", b"133121",
|
|
18
|
+
b"313121", b"211331", b"231131", b"213113", b"213311", b"213131", b"311123", b"311321",
|
|
19
|
+
b"331121", b"312113", b"312311", b"332111", b"314111", b"221411", b"431111", b"111224",
|
|
20
|
+
b"111422", b"121124", b"121421", b"141122", b"141221", b"112214", b"112412", b"122114",
|
|
21
|
+
b"122411", b"142112", b"142211", b"241211", b"221114", b"413111", b"241112", b"134111",
|
|
22
|
+
b"111242", b"121142", b"121241", b"114212", b"124112", b"124211", b"411212", b"421112",
|
|
23
|
+
b"421211", b"212141", b"214121", b"412121", b"111143", b"111341", b"131141", b"114113",
|
|
24
|
+
b"114311", b"411113", b"411311", b"113141", b"114131", b"311141", b"411131", b"211412",
|
|
25
|
+
b"211214", b"211232",
|
|
26
|
+
];
|
|
27
|
+
const STOP: &[u8; 7] = b"2331112";
|
|
28
|
+
const CODE_C: u8 = 99;
|
|
29
|
+
const CODE_B: u8 = 100;
|
|
30
|
+
const CODE_A: u8 = 101;
|
|
31
|
+
const FNC1: u8 = 102;
|
|
32
|
+
const START_A: u8 = 103;
|
|
33
|
+
|
|
34
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
35
|
+
enum Set {
|
|
36
|
+
A,
|
|
37
|
+
B,
|
|
38
|
+
C,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/// The modules of `units`: start, data, check symbol, stop.
|
|
42
|
+
pub fn encode(units: &[Unit]) -> Result<Vec<bool>> {
|
|
43
|
+
if let Some(byte) = units.iter().find_map(|unit| match unit {
|
|
44
|
+
Unit::Char(byte) if !byte.is_ascii() => Some(*byte),
|
|
45
|
+
_ => None,
|
|
46
|
+
}) {
|
|
47
|
+
let reason = format!("it holds ASCII only, not byte 0x{byte:02X}");
|
|
48
|
+
return Err(Symbology::Code128.invalid(reason));
|
|
49
|
+
}
|
|
50
|
+
let mut set = start(units);
|
|
51
|
+
let mut values = vec![START_A + set as u8];
|
|
52
|
+
let mut at = 0;
|
|
53
|
+
while let Some(&unit) = units.get(at) {
|
|
54
|
+
match (set, unit) {
|
|
55
|
+
(_, Unit::Fnc1) => {
|
|
56
|
+
values.push(FNC1);
|
|
57
|
+
at += 1;
|
|
58
|
+
}
|
|
59
|
+
(Set::C, Unit::Char(_)) => {
|
|
60
|
+
if let Some(pair) = digit_pair(units, at) {
|
|
61
|
+
values.push(pair);
|
|
62
|
+
at += 2;
|
|
63
|
+
} else {
|
|
64
|
+
set = text_set(units, at);
|
|
65
|
+
values.push(code_symbol(set));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
(Set::A | Set::B, Unit::Char(byte)) => {
|
|
69
|
+
// Annex E: four digits or more go to set C, the first of an
|
|
70
|
+
// odd run first.
|
|
71
|
+
let run = digit_run(units, at);
|
|
72
|
+
if run >= 4 && run.is_multiple_of(2) {
|
|
73
|
+
set = Set::C;
|
|
74
|
+
values.push(CODE_C);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if !encodable(set, byte) {
|
|
78
|
+
set = if set == Set::A { Set::B } else { Set::A };
|
|
79
|
+
values.push(code_symbol(set));
|
|
80
|
+
}
|
|
81
|
+
values.push(value(set, byte));
|
|
82
|
+
at += 1;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
let weighted: u32 = values
|
|
87
|
+
.iter()
|
|
88
|
+
.zip(0_u32..)
|
|
89
|
+
.map(|(&value, position)| u32::from(value) * position.max(1))
|
|
90
|
+
.sum();
|
|
91
|
+
values.push(u8::try_from(weighted % 103).unwrap_or_default());
|
|
92
|
+
|
|
93
|
+
let mut modules = Vec::with_capacity(values.len() * 11 + 13);
|
|
94
|
+
for value in values {
|
|
95
|
+
widths(&mut modules, PATTERNS[usize::from(value)]);
|
|
96
|
+
}
|
|
97
|
+
widths(&mut modules, STOP);
|
|
98
|
+
Ok(modules)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/// Set C when the data, past a leading FNC1, opens with four digits or more
|
|
102
|
+
/// (or is an even number of digits); else as [`text_set`].
|
|
103
|
+
fn start(units: &[Unit]) -> Set {
|
|
104
|
+
let first = units.iter().position(|unit| *unit != Unit::Fnc1).unwrap_or(units.len());
|
|
105
|
+
let run = digit_run(units, first);
|
|
106
|
+
let all_digits = first + run == units.len() && run.is_multiple_of(2) && run > 0;
|
|
107
|
+
if run >= 4 || all_digits { Set::C } else { text_set(units, first) }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/// Set A when a control character comes before any lower case letter, else
|
|
111
|
+
/// set B.
|
|
112
|
+
fn text_set(units: &[Unit], from: usize) -> Set {
|
|
113
|
+
let first = units[from..].iter().find_map(|unit| match unit {
|
|
114
|
+
Unit::Char(byte) if *byte < 32 => Some(Set::A),
|
|
115
|
+
Unit::Char(byte) if *byte >= 96 => Some(Set::B),
|
|
116
|
+
_ => None,
|
|
117
|
+
});
|
|
118
|
+
first.unwrap_or(Set::B)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
fn digit_run(units: &[Unit], from: usize) -> usize {
|
|
122
|
+
let digit = |unit: &&Unit| matches!(unit, Unit::Char(byte) if byte.is_ascii_digit());
|
|
123
|
+
units.get(from..).map_or(0, |rest| rest.iter().take_while(digit).count())
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
fn digit_pair(units: &[Unit], at: usize) -> Option<u8> {
|
|
127
|
+
match units.get(at..at + 2)? {
|
|
128
|
+
[Unit::Char(tens), Unit::Char(ones)] if tens.is_ascii_digit() && ones.is_ascii_digit() => {
|
|
129
|
+
Some((tens - b'0') * 10 + (ones - b'0'))
|
|
130
|
+
}
|
|
131
|
+
_ => None,
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/// The symbol that switches into set A or B (sets are otherwise only
|
|
136
|
+
/// entered from set C, by [`CODE_C`]).
|
|
137
|
+
const fn code_symbol(set: Set) -> u8 {
|
|
138
|
+
if matches!(set, Set::A) { CODE_A } else { CODE_B }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const fn encodable(set: Set, byte: u8) -> bool {
|
|
142
|
+
match set {
|
|
143
|
+
Set::A => byte < 96,
|
|
144
|
+
Set::B | Set::C => byte >= 32,
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/// A character's value in set A or B.
|
|
149
|
+
const fn value(set: Set, byte: u8) -> u8 {
|
|
150
|
+
match set {
|
|
151
|
+
Set::A if byte < 32 => byte + 64,
|
|
152
|
+
_ => byte - 32,
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/// Bars and spaces of these widths, a bar first.
|
|
157
|
+
fn widths(modules: &mut Vec<bool>, widths: &[u8]) {
|
|
158
|
+
for (index, width) in widths.iter().enumerate() {
|
|
159
|
+
let bar = index % 2 == 0;
|
|
160
|
+
modules.extend(std::iter::repeat_n(bar, usize::from(width - b'0')));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
//! A symbol as rectangles in module units, and those as SVG or PNG; PDF
|
|
2
|
+
//! draws the same rectangles.
|
|
3
|
+
|
|
4
|
+
use std::fmt::Write as _;
|
|
5
|
+
|
|
6
|
+
use super::{Barcode, Look};
|
|
7
|
+
use crate::read::{PngColor, png_with_transparency};
|
|
8
|
+
|
|
9
|
+
/// The largest side, in pixels, a PNG may have, as pictures.
|
|
10
|
+
const MAX_PIXELS: usize = 1 << 16;
|
|
11
|
+
|
|
12
|
+
/// A symbol in module units: its whole width and height, quiet zone
|
|
13
|
+
/// included, and its dark rectangles `[x, y, width, height]` from the top
|
|
14
|
+
/// left. Each row's runs are one rectangle; a linear code's bars span its
|
|
15
|
+
/// height.
|
|
16
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
17
|
+
pub struct Shapes {
|
|
18
|
+
pub width: usize,
|
|
19
|
+
pub height: usize,
|
|
20
|
+
pub rects: Vec<[usize; 4]>,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
impl Shapes {
|
|
24
|
+
/// Its width and height in modules, for scaling.
|
|
25
|
+
#[expect(clippy::cast_precision_loss, reason = "a symbol is far below 2**52 modules")]
|
|
26
|
+
pub const fn extent(&self) -> (f64, f64) {
|
|
27
|
+
(self.width as f64, self.height as f64)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
impl Barcode {
|
|
32
|
+
pub(crate) fn shapes(&self, quiet_zone: bool, bar_height: Option<u32>) -> Shapes {
|
|
33
|
+
let (left, right, edge) = if quiet_zone { self.symbology.quiet_zone() } else { (0, 0, 0) };
|
|
34
|
+
let row_height = if self.symbology.is_linear() {
|
|
35
|
+
bar_height.map_or_else(|| self.default_bar_height(), |bars| bars.max(1) as usize)
|
|
36
|
+
} else {
|
|
37
|
+
1
|
|
38
|
+
};
|
|
39
|
+
let mut rects = Vec::new();
|
|
40
|
+
for (y, row) in self.modules.chunks(self.columns).enumerate() {
|
|
41
|
+
let mut x = 0;
|
|
42
|
+
while x < row.len() {
|
|
43
|
+
let run = row[x..].iter().take_while(|&&dark| dark == row[x]).count();
|
|
44
|
+
if row[x] {
|
|
45
|
+
rects.push([left + x, edge + y * row_height, run, row_height]);
|
|
46
|
+
}
|
|
47
|
+
x += run;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
Shapes {
|
|
51
|
+
width: left + self.columns + right,
|
|
52
|
+
height: 2 * edge + self.rows * row_height,
|
|
53
|
+
rects,
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// `module` user units a module, the rectangles as one path.
|
|
59
|
+
pub fn svg(shapes: &Shapes, module: f64, look: &Look) -> String {
|
|
60
|
+
let (width, height) = (shapes.width, shapes.height);
|
|
61
|
+
let (columns, rows) = shapes.extent();
|
|
62
|
+
let (pixels_wide, pixels_high) = (number(columns * module), number(rows * module));
|
|
63
|
+
let mut out = format!(
|
|
64
|
+
"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"{pixels_wide}\" \
|
|
65
|
+
height=\"{pixels_high}\" viewBox=\"0 0 {width} {height}\" \
|
|
66
|
+
shape-rendering=\"crispEdges\">"
|
|
67
|
+
);
|
|
68
|
+
if let Some(light) = look.light {
|
|
69
|
+
let fill = light.hex();
|
|
70
|
+
// write! into a String cannot fail.
|
|
71
|
+
let _ = write!(out, "<rect width=\"{width}\" height=\"{height}\" fill=\"{fill}\"/>");
|
|
72
|
+
}
|
|
73
|
+
// write! into a String cannot fail.
|
|
74
|
+
let _ = write!(out, "<path fill=\"{}\" d=\"", look.dark.hex());
|
|
75
|
+
for [x, y, run, rows] in &shapes.rects {
|
|
76
|
+
// write! into a String cannot fail.
|
|
77
|
+
let _ = write!(out, "M{x} {y}h{run}v{rows}h-{run}z");
|
|
78
|
+
}
|
|
79
|
+
out.push_str("\"/></svg>");
|
|
80
|
+
out
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/// `scale` pixels a module, one bit a pixel: palette entry 0 the light
|
|
84
|
+
/// color (transparent without one), 1 the dark. `None` past
|
|
85
|
+
/// [`MAX_PIXELS`] or at a scale of 0.
|
|
86
|
+
pub fn png(shapes: &Shapes, scale: u32, look: &Look) -> Option<Vec<u8>> {
|
|
87
|
+
let (pixels_wide, pixels_high, rows) = bits(shapes, scale)?;
|
|
88
|
+
let light = look.light.unwrap_or(crate::style::Color::WHITE).bytes();
|
|
89
|
+
let palette = [light, look.dark.bytes()].concat();
|
|
90
|
+
let transparency: &[u8] = if look.light.is_none() { &[0] } else { &[] };
|
|
91
|
+
let (width, height) = (u32::try_from(pixels_wide).ok()?, u32::try_from(pixels_high).ok()?);
|
|
92
|
+
Some(png_with_transparency(
|
|
93
|
+
width,
|
|
94
|
+
height,
|
|
95
|
+
PngColor::Palette,
|
|
96
|
+
1,
|
|
97
|
+
Some(&palette),
|
|
98
|
+
transparency,
|
|
99
|
+
&rows,
|
|
100
|
+
))
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/// The pixels, one bit each, rows starting on a byte: width, height, rows.
|
|
104
|
+
fn bits(shapes: &Shapes, scale: u32) -> Option<(usize, usize, Vec<u8>)> {
|
|
105
|
+
let scale = usize::try_from(scale).ok().filter(|&scale| scale > 0)?;
|
|
106
|
+
let (width, height) = (shapes.width * scale, shapes.height * scale);
|
|
107
|
+
(width <= MAX_PIXELS && height <= MAX_PIXELS).then_some(())?;
|
|
108
|
+
let stride = width.div_ceil(8);
|
|
109
|
+
let mut rows = vec![0_u8; stride * height];
|
|
110
|
+
for &[x, y, run, run_rows] in &shapes.rects {
|
|
111
|
+
for row in y * scale..(y + run_rows) * scale {
|
|
112
|
+
for pixel in x * scale..(x + run) * scale {
|
|
113
|
+
rows[row * stride + pixel / 8] |= 0x80 >> (pixel % 8);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
Some((width, height, rows))
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/// Up to three decimals, without trailing zeros.
|
|
121
|
+
fn number(value: f64) -> String {
|
|
122
|
+
let text = format!("{value:.3}");
|
|
123
|
+
text.trim_end_matches('0').trim_end_matches('.').to_owned()
|
|
124
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
//! EAN-13, EAN-8 and UPC-A (ISO/IEC 15420): GTINs as retail scanners read
|
|
2
|
+
//! them, digits in seven-module patterns between guard bars.
|
|
3
|
+
|
|
4
|
+
use super::Symbology;
|
|
5
|
+
use super::gs1::check_digit;
|
|
6
|
+
use crate::Result;
|
|
7
|
+
|
|
8
|
+
/// The left-hand, odd-parity patterns (set A) of 0 to 9. The right-hand
|
|
9
|
+
/// patterns (set C) are their inverses; the even-parity ones (set B), the
|
|
10
|
+
/// right-hand ones mirrored.
|
|
11
|
+
const SET_A: [u8; 10] = [
|
|
12
|
+
0b000_1101, 0b001_1001, 0b001_0011, 0b011_1101, 0b010_0011, 0b011_0001, 0b010_1111, 0b011_1011,
|
|
13
|
+
0b011_0111, 0b000_1011,
|
|
14
|
+
];
|
|
15
|
+
/// Which of EAN-13's left six digits take set B, the first in the high
|
|
16
|
+
/// bit, by its leading digit (the one no pattern draws).
|
|
17
|
+
const PARITY: [u8; 10] = [
|
|
18
|
+
0b00_0000, 0b00_1011, 0b00_1101, 0b00_1110, 0b01_0011, 0b01_1001, 0b01_1100, 0b01_0101,
|
|
19
|
+
0b01_0110, 0b01_1010,
|
|
20
|
+
];
|
|
21
|
+
const GUARD: [bool; 3] = [true, false, true];
|
|
22
|
+
const CENTER: [bool; 5] = [false, true, false, true, false];
|
|
23
|
+
|
|
24
|
+
/// `input` as digits: one short of `length` gets its check digit, `length`
|
|
25
|
+
/// must end with the right one.
|
|
26
|
+
pub fn digits(symbology: Symbology, input: &str, length: usize) -> Result<Vec<u8>> {
|
|
27
|
+
input
|
|
28
|
+
.bytes()
|
|
29
|
+
.all(|byte| byte.is_ascii_digit())
|
|
30
|
+
.ok_or_else(|| symbology.invalid(format!("{input:?} is not all digits")))?;
|
|
31
|
+
(input.len() == length - 1 || input.len() == length).ok_or_else(|| {
|
|
32
|
+
let (short, given) = (length - 1, input.len());
|
|
33
|
+
symbology.invalid(format!(
|
|
34
|
+
"it takes {short} digits, or {length} with the check digit, not {given}"
|
|
35
|
+
))
|
|
36
|
+
})?;
|
|
37
|
+
let check = check_digit(&input[..length - 1]).unwrap_or_default();
|
|
38
|
+
let mut digits: Vec<u8> = input.bytes().map(|byte| byte - b'0').collect();
|
|
39
|
+
match digits.get(length - 1) {
|
|
40
|
+
None => digits.push(check),
|
|
41
|
+
Some(&last) if last != check => {
|
|
42
|
+
return Err(symbology.invalid(format!("{input} has check digit {last}, not {check}")));
|
|
43
|
+
}
|
|
44
|
+
Some(_) => {}
|
|
45
|
+
}
|
|
46
|
+
Ok(digits)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
pub fn text(digits: &[u8]) -> String {
|
|
50
|
+
digits.iter().map(|digit| char::from(b'0' + digit)).collect()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/// 95 modules: guard, six left digits by the first's parity, center, six
|
|
54
|
+
/// right digits, guard.
|
|
55
|
+
pub fn ean13(digits: &[u8]) -> Vec<bool> {
|
|
56
|
+
let parity = PARITY[usize::from(digits[0])];
|
|
57
|
+
let mut modules = Vec::with_capacity(95);
|
|
58
|
+
modules.extend(GUARD);
|
|
59
|
+
for (index, &digit) in digits[1..7].iter().enumerate() {
|
|
60
|
+
let even = (parity >> (5 - index)) & 1 == 1;
|
|
61
|
+
push(&mut modules, if even { set_b(digit) } else { SET_A[usize::from(digit)] });
|
|
62
|
+
}
|
|
63
|
+
modules.extend(CENTER);
|
|
64
|
+
digits[7..].iter().for_each(|&digit| push(&mut modules, set_c(digit)));
|
|
65
|
+
modules.extend(GUARD);
|
|
66
|
+
modules
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/// 67 modules: guard, four digits in set A, center, four in set C, guard.
|
|
70
|
+
pub fn ean8(digits: &[u8]) -> Vec<bool> {
|
|
71
|
+
let mut modules = Vec::with_capacity(67);
|
|
72
|
+
modules.extend(GUARD);
|
|
73
|
+
digits[..4].iter().for_each(|&digit| push(&mut modules, SET_A[usize::from(digit)]));
|
|
74
|
+
modules.extend(CENTER);
|
|
75
|
+
digits[4..].iter().for_each(|&digit| push(&mut modules, set_c(digit)));
|
|
76
|
+
modules.extend(GUARD);
|
|
77
|
+
modules
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const fn set_c(digit: u8) -> u8 {
|
|
81
|
+
!SET_A[digit as usize] & 0x7F
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const fn set_b(digit: u8) -> u8 {
|
|
85
|
+
set_c(digit).reverse_bits() >> 1
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// A seven-module pattern, its high bit first.
|
|
89
|
+
fn push(modules: &mut Vec<bool>, pattern: u8) {
|
|
90
|
+
modules.extend((0..7).rev().map(|bit| (pattern >> bit) & 1 == 1));
|
|
91
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
//! GS1 element strings as people write them, `(01)09506000134352(10)AB12`:
|
|
2
|
+
//! Application Identifiers in parentheses, each followed by its value.
|
|
3
|
+
|
|
4
|
+
#[cfg(test)]
|
|
5
|
+
mod tests;
|
|
6
|
+
|
|
7
|
+
use crate::{Error, Result};
|
|
8
|
+
|
|
9
|
+
/// What a symbol carries: characters, and FNC1, which ends every
|
|
10
|
+
/// variable-length field but the last.
|
|
11
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
12
|
+
pub enum Unit {
|
|
13
|
+
Char(u8),
|
|
14
|
+
Fnc1,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/// One Application Identifier and its value.
|
|
18
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
19
|
+
pub struct Field<'a> {
|
|
20
|
+
pub ai: &'a str,
|
|
21
|
+
pub value: &'a str,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// Element strings whose length, AI included, the AI's first two digits
|
|
25
|
+
/// fix (GS1 General Specifications, figure 7.8.5-2). All are numeric; the
|
|
26
|
+
/// rest end with FNC1 unless last.
|
|
27
|
+
const PREDEFINED: [(&str, usize); 22] = [
|
|
28
|
+
("00", 20),
|
|
29
|
+
("01", 16),
|
|
30
|
+
("02", 16),
|
|
31
|
+
("03", 16),
|
|
32
|
+
("04", 18),
|
|
33
|
+
("11", 8),
|
|
34
|
+
("12", 8),
|
|
35
|
+
("13", 8),
|
|
36
|
+
("14", 8),
|
|
37
|
+
("15", 8),
|
|
38
|
+
("16", 8),
|
|
39
|
+
("17", 8),
|
|
40
|
+
("18", 8),
|
|
41
|
+
("19", 8),
|
|
42
|
+
("20", 4),
|
|
43
|
+
("31", 10),
|
|
44
|
+
("32", 10),
|
|
45
|
+
("33", 10),
|
|
46
|
+
("34", 10),
|
|
47
|
+
("35", 10),
|
|
48
|
+
("36", 10),
|
|
49
|
+
("41", 16),
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
/// The longest value any AI takes.
|
|
53
|
+
const MAX_VALUE: usize = 90;
|
|
54
|
+
|
|
55
|
+
/// The GS1 check digit (modulo 10, weights 3 and 1 from the right) of
|
|
56
|
+
/// `digits`, as GTINs, SSCCs and GLNs end with; `None` unless all are
|
|
57
|
+
/// digits.
|
|
58
|
+
///
|
|
59
|
+
/// ```
|
|
60
|
+
/// assert_eq!(imprimatur::barcode::check_digit("950600013435"), Some(2));
|
|
61
|
+
/// ```
|
|
62
|
+
#[must_use]
|
|
63
|
+
pub fn check_digit(digits: &str) -> Option<u8> {
|
|
64
|
+
let mut sum = 0_u32;
|
|
65
|
+
for (index, byte) in digits.bytes().rev().enumerate() {
|
|
66
|
+
byte.is_ascii_digit().then_some(())?;
|
|
67
|
+
let weight = if index % 2 == 0 { 3 } else { 1 };
|
|
68
|
+
sum += weight * u32::from(byte - b'0');
|
|
69
|
+
}
|
|
70
|
+
u8::try_from((10 - sum % 10) % 10).ok()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/// Reads an element string into its fields.
|
|
74
|
+
///
|
|
75
|
+
/// # Errors
|
|
76
|
+
/// [`Error::InvalidElementString`] for anything but AIs of 2 to 4 digits in
|
|
77
|
+
/// parentheses, each with a value in GS1's character set, of the length
|
|
78
|
+
/// and check digit its AI takes.
|
|
79
|
+
pub fn parse(input: &str) -> Result<Vec<Field<'_>>> {
|
|
80
|
+
let invalid = |reason: String| Error::InvalidElementString(reason);
|
|
81
|
+
let bytes = input.as_bytes();
|
|
82
|
+
(!bytes.is_empty()).ok_or_else(|| invalid("it is empty".to_owned()))?;
|
|
83
|
+
let mut fields = Vec::new();
|
|
84
|
+
let mut at = 0;
|
|
85
|
+
while at < bytes.len() {
|
|
86
|
+
let length = ai_at(bytes, at).ok_or_else(|| {
|
|
87
|
+
invalid(format!("expected an AI such as (01) at character {}", at + 1))
|
|
88
|
+
})?;
|
|
89
|
+
let start = at + length + 2;
|
|
90
|
+
let end = (start..bytes.len()).find(|&next| ai_at(bytes, next).is_some());
|
|
91
|
+
let end = end.unwrap_or(bytes.len());
|
|
92
|
+
let field = Field { ai: &input[at + 1..at + 1 + length], value: &input[start..end] };
|
|
93
|
+
validate(field)?;
|
|
94
|
+
fields.push(field);
|
|
95
|
+
at = end;
|
|
96
|
+
}
|
|
97
|
+
Ok(fields)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/// `element_string`, parsed and run together as [`units`] does: what every
|
|
101
|
+
/// encoder needs before laying its symbol out.
|
|
102
|
+
///
|
|
103
|
+
/// # Errors
|
|
104
|
+
/// [`Error::InvalidElementString`], as [`parse`].
|
|
105
|
+
pub fn element_units(element_string: &str) -> Result<Vec<Unit>> {
|
|
106
|
+
Ok(units(&parse(element_string)?))
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/// The fields run together: no parentheses, FNC1 after each field of
|
|
110
|
+
/// variable length but the last.
|
|
111
|
+
pub fn units(fields: &[Field<'_>]) -> Vec<Unit> {
|
|
112
|
+
let mut units = Vec::new();
|
|
113
|
+
for (index, field) in fields.iter().enumerate() {
|
|
114
|
+
units.extend(field.ai.bytes().chain(field.value.bytes()).map(Unit::Char));
|
|
115
|
+
if predefined(field.ai).is_none() && index + 1 < fields.len() {
|
|
116
|
+
units.push(Unit::Fnc1);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
units
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/// The length of the AI in parentheses at `at`: 2 to 4 digits.
|
|
123
|
+
fn ai_at(bytes: &[u8], at: usize) -> Option<usize> {
|
|
124
|
+
(bytes.get(at) == Some(&b'(')).then_some(())?;
|
|
125
|
+
let digits = bytes[at + 1..].iter().take_while(|byte| byte.is_ascii_digit()).count();
|
|
126
|
+
((2..=4).contains(&digits) && bytes.get(at + 1 + digits) == Some(&b')')).then_some(digits)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/// The length of the element string, AI included, when the AI fixes it.
|
|
130
|
+
fn predefined(ai: &str) -> Option<usize> {
|
|
131
|
+
PREDEFINED.iter().find(|(prefix, _)| ai.starts_with(prefix)).map(|&(_, length)| length)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
fn validate(field: Field<'_>) -> Result<()> {
|
|
135
|
+
let Field { ai, value } = field;
|
|
136
|
+
let invalid = |reason: String| Err(Error::InvalidElementString(format!("({ai}) {reason}")));
|
|
137
|
+
if value.is_empty() {
|
|
138
|
+
return invalid("has no value".to_owned());
|
|
139
|
+
}
|
|
140
|
+
let outside = |character: char| !u8::try_from(character).is_ok_and(in_character_set);
|
|
141
|
+
if let Some(character) = value.chars().find(|&character| outside(character)) {
|
|
142
|
+
return invalid(format!("holds {character:?}, outside GS1's character set"));
|
|
143
|
+
}
|
|
144
|
+
if value.len() > MAX_VALUE {
|
|
145
|
+
return invalid(format!("holds {} characters, more than {MAX_VALUE}", value.len()));
|
|
146
|
+
}
|
|
147
|
+
let Some(length) = predefined(ai) else { return Ok(()) };
|
|
148
|
+
let digits = length - ai.len();
|
|
149
|
+
if value.len() != digits || !value.bytes().all(|byte| byte.is_ascii_digit()) {
|
|
150
|
+
return invalid(format!("takes {digits} digits, not {value:?}"));
|
|
151
|
+
}
|
|
152
|
+
// SSCC, GTINs and GLNs end with a check digit.
|
|
153
|
+
let checked = matches!(ai, "00" | "01" | "02") || ai.starts_with("41");
|
|
154
|
+
let (body, last) = value.split_at(value.len() - 1);
|
|
155
|
+
let expected = check_digit(body).unwrap_or(0);
|
|
156
|
+
if checked && last.as_bytes()[0] - b'0' != expected {
|
|
157
|
+
return invalid(format!("{value} has check digit {last}, not {expected}"));
|
|
158
|
+
}
|
|
159
|
+
Ok(())
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/// GS1 AI encodable character set 82: letters, digits and
|
|
163
|
+
/// ``!"%&'()*+,-./:;<=>?_``.
|
|
164
|
+
const fn in_character_set(byte: u8) -> bool {
|
|
165
|
+
byte.is_ascii_alphanumeric()
|
|
166
|
+
|| matches!(
|
|
167
|
+
byte,
|
|
168
|
+
b'!' | b'"'
|
|
169
|
+
| b'%'
|
|
170
|
+
| b'&'
|
|
171
|
+
| b'\''
|
|
172
|
+
| b'('
|
|
173
|
+
| b')'
|
|
174
|
+
| b'*'
|
|
175
|
+
| b'+'
|
|
176
|
+
| b','
|
|
177
|
+
| b'-'
|
|
178
|
+
| b'.'
|
|
179
|
+
| b'/'
|
|
180
|
+
| b':'
|
|
181
|
+
| b';'
|
|
182
|
+
| b'<'
|
|
183
|
+
| b'='
|
|
184
|
+
| b'>'
|
|
185
|
+
| b'?'
|
|
186
|
+
| b'_'
|
|
187
|
+
)
|
|
188
|
+
}
|