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,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
|
+
}
|
|
@@ -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
|
+
}
|