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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +9 -0
  3. data/Cargo.lock +33 -3
  4. data/Cargo.toml +1 -1
  5. data/README.md +41 -1
  6. data/ext/imprimatur_native/cli/Cargo.toml +3 -1
  7. data/ext/imprimatur_native/core/Cargo.toml +15 -4
  8. data/ext/imprimatur_native/core/README.md +27 -4
  9. data/ext/imprimatur_native/core/src/barcode/code128.rs +162 -0
  10. data/ext/imprimatur_native/core/src/barcode/draw.rs +124 -0
  11. data/ext/imprimatur_native/core/src/barcode/ean.rs +91 -0
  12. data/ext/imprimatur_native/core/src/barcode/gs1.rs +188 -0
  13. data/ext/imprimatur_native/core/src/barcode/matrix.rs +130 -0
  14. data/ext/imprimatur_native/core/src/barcode.rs +314 -0
  15. data/ext/imprimatur_native/core/src/doc/symbol.rs +136 -0
  16. data/ext/imprimatur_native/core/src/doc.rs +11 -1
  17. data/ext/imprimatur_native/core/src/docx.rs +22 -10
  18. data/ext/imprimatur_native/core/src/error.rs +6 -0
  19. data/ext/imprimatur_native/core/src/font.rs +3 -1
  20. data/ext/imprimatur_native/core/src/lib.rs +3 -1
  21. data/ext/imprimatur_native/core/src/pdf/layout.rs +48 -14
  22. data/ext/imprimatur_native/core/src/pdf/truetype/descriptor.rs +129 -0
  23. data/ext/imprimatur_native/core/src/pdf/truetype/program.rs +70 -0
  24. data/ext/imprimatur_native/core/src/pdf/truetype.rs +149 -0
  25. data/ext/imprimatur_native/core/src/pdf.rs +9 -1
  26. data/ext/imprimatur_native/core/src/read/image.rs +17 -0
  27. data/ext/imprimatur_native/core/src/read.rs +3 -2
  28. data/ext/imprimatur_native/core/src/style.rs +11 -0
  29. data/ext/imprimatur_native/ffi/Cargo.toml +1 -1
  30. data/ext/imprimatur_native/ffi/src/barcode.rs +147 -0
  31. data/ext/imprimatur_native/ffi/src/document.rs +29 -1
  32. data/ext/imprimatur_native/ffi/src/lib.rs +5 -3
  33. data/lib/imprimatur/barcode.rb +78 -0
  34. data/lib/imprimatur/document.rb +17 -1
  35. data/lib/imprimatur/version.rb +1 -1
  36. data/lib/imprimatur.rb +4 -0
  37. metadata +13 -2
  38. data/ext/imprimatur_native/core/src/test_support.rs +0 -251
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 72d405b951859304a851be99269f01972fd7817960cb53f01dbfca257f700112
4
- data.tar.gz: 5dbc90d5aec863460a9d61134961d0c384aafd073890d52023b9291aa7ba24b4
3
+ metadata.gz: 4b0c00d27618298889f1419aec02a69a9de179fb9f3caaa5f33e57db05a68a9b
4
+ data.tar.gz: f8c77e28a3f3ada14611260a7f1ffffc830ba3f6e0ec6d0675b4c8177b391f72
5
5
  SHA512:
6
- metadata.gz: 1e1ea6bb4b63996aea9b36d1241e34c7133cbf3f2305778542c1267fdea7b62db170a8434d4af174fc8d49930a348a2682f07f0b8bec1d2be80c8696ebd75664
7
- data.tar.gz: bc7c2139ba52cf9f33256c7d23972d0b2eaa55a7252e810d0cca1897ed5405bacd64b22b8eacd9636c938c39cf241051af1ae68b4d5a0e81595fffd8d1a61964
6
+ metadata.gz: 4780e0a1754d6b1df1562910bf63f1cf70b2d7c5750c1d04e6518898408eded1d13ced430564a966f3980fa1b5db0af131aba34db66de50ca0ec19cd4db1c827
7
+ data.tar.gz: a4a2dde579ad49d425a2e22e39907cf35041061d245b41c8cde6f1abd3c2abdcda1c0e3c28bafcbdfe6a2bf55eab5a5adc7e30750af5fcbb1739216d5c2708d6
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.0](https://github.com/seuros/imprimatur/compare/v0.3.0...v0.4.0) (2026-10-18)
4
+
5
+
6
+ ### Features
7
+
8
+ * barcodes: QR Code, Data Matrix, EAN-13, EAN-8, UPC-A and Code 128, GS1 forms included, as SVG, PNG and in documents ([ade7b68](https://github.com/seuros/imprimatur/commit/ade7b68f4c5adcab9d2ee0b935288eea7ec2eea6))
9
+ * **pdf:** add embedded TrueType font resources ([c0e3be4](https://github.com/seuros/imprimatur/commit/c0e3be4ef46ede94fcc840840e82d8ccb6679f3e))
10
+ * **pdf:** add embedded TrueType font resources ([313075f](https://github.com/seuros/imprimatur/commit/313075fe8ba1bedc9f4156060aeedcbb0e0a8bef))
11
+
3
12
  ## [0.3.0](https://github.com/seuros/imprimatur/compare/v0.2.0...v0.3.0) (2026-10-17)
4
13
 
5
14
 
data/Cargo.lock CHANGED
@@ -28,6 +28,12 @@ dependencies = [
28
28
  "memchr",
29
29
  ]
30
30
 
31
+ [[package]]
32
+ name = "arrayvec"
33
+ version = "0.7.8"
34
+ source = "registry+https://github.com/rust-lang/crates.io-index"
35
+ checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56"
36
+
31
37
  [[package]]
32
38
  name = "autocfg"
33
39
  version = "1.5.1"
@@ -168,6 +174,16 @@ dependencies = [
168
174
  "hybrid-array",
169
175
  ]
170
176
 
177
+ [[package]]
178
+ name = "datamatrix"
179
+ version = "0.3.3"
180
+ source = "registry+https://github.com/rust-lang/crates.io-index"
181
+ checksum = "b2d3882a013aca40e2e24f0e454ef7a581a50c603d67eded130013ab641a289d"
182
+ dependencies = [
183
+ "arrayvec",
184
+ "flagset",
185
+ ]
186
+
171
187
  [[package]]
172
188
  name = "digest"
173
189
  version = "0.11.3"
@@ -207,6 +223,12 @@ version = "2.5.0"
207
223
  source = "registry+https://github.com/rust-lang/crates.io-index"
208
224
  checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
209
225
 
226
+ [[package]]
227
+ name = "flagset"
228
+ version = "0.4.7"
229
+ source = "registry+https://github.com/rust-lang/crates.io-index"
230
+ checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe"
231
+
210
232
  [[package]]
211
233
  name = "flate2"
212
234
  version = "1.1.10"
@@ -274,13 +296,15 @@ dependencies = [
274
296
 
275
297
  [[package]]
276
298
  name = "imprimatur"
277
- version = "0.3.0"
299
+ version = "0.4.0"
278
300
  dependencies = [
279
301
  "aes",
280
302
  "crc32fast",
303
+ "datamatrix",
281
304
  "font-test-data",
282
305
  "md-5",
283
306
  "miniz_oxide",
307
+ "qrcode",
284
308
  "read-fonts",
285
309
  "serde_json",
286
310
  "sha2",
@@ -291,7 +315,7 @@ dependencies = [
291
315
 
292
316
  [[package]]
293
317
  name = "imprimatur-cli"
294
- version = "0.3.0"
318
+ version = "0.4.0"
295
319
  dependencies = [
296
320
  "imprimatur",
297
321
  "regex",
@@ -303,7 +327,7 @@ dependencies = [
303
327
 
304
328
  [[package]]
305
329
  name = "imprimatur_native"
306
- version = "0.3.0"
330
+ version = "0.4.0"
307
331
  dependencies = [
308
332
  "imprimatur",
309
333
  "magnus",
@@ -501,6 +525,12 @@ dependencies = [
501
525
  "unicase",
502
526
  ]
503
527
 
528
+ [[package]]
529
+ name = "qrcode"
530
+ version = "0.14.1"
531
+ source = "registry+https://github.com/rust-lang/crates.io-index"
532
+ checksum = "d68782463e408eb1e668cf6152704bd856c78c5b6417adaee3203d8f4c1fc9ec"
533
+
504
534
  [[package]]
505
535
  name = "quote"
506
536
  version = "1.0.47"
data/Cargo.toml CHANGED
@@ -10,7 +10,7 @@ license = "BSD-3-Clause"
10
10
  repository = "https://github.com/seuros/imprimatur"
11
11
 
12
12
  [workspace.dependencies]
13
- imprimatur = { path = "ext/imprimatur_native/core", version = "0.3.0" }
13
+ imprimatur = { path = "ext/imprimatur_native/core", version = "0.4.0" }
14
14
  magnus = { version = "0.9.2", default-features = false }
15
15
  # For rb_thread_call_without_gvl2, which magnus does not wrap; magnus's own
16
16
  # rb-sys, features and all.
data/README.md CHANGED
@@ -9,7 +9,8 @@ a 40,000-character cell is refused at the door, not discovered by Excel's
9
9
  as `imprimatur` on crates.io; the gem wraps it.
10
10
  - **No third-party format stack.** The XML writer and the zip container are
11
11
  written here, from ECMA-376 and the zip specification; only deflate
12
- (`miniz_oxide`) and CRC-32 (`crc32fast`) come from crates.
12
+ (`miniz_oxide`) and CRC-32 (`crc32fast`) come from crates, and QR Code and
13
+ Data Matrix encoding (`qrcode`, `datamatrix`) for barcodes.
13
14
  - **Deterministic.** The same document always gives the same bytes.
14
15
  - **Native only.** CRuby 4.0+. No pure Ruby fallback, no JRuby, no TruffleRuby.
15
16
 
@@ -223,6 +224,45 @@ dashes, the euro sign. Anything else (Arabic, Hebrew, Greek, CJK, emoji) raises
223
224
  script. Embedded TrueType fonts, and with them every script and proper RTL in
224
225
  PDF, are the next step.
225
226
 
227
+ ## Barcodes
228
+
229
+ QR Code, Data Matrix, EAN-13, EAN-8, UPC-A and Code 128, and the GS1 forms of
230
+ QR Code, Data Matrix and Code 128 (GS1-128):
231
+
232
+ ```ruby
233
+ qr = Imprimatur.barcode(:qr, "https://id.gs1.org/01/09506000134352", level: :m)
234
+ File.write("qr.svg", qr.svg(module_size: 4))
235
+ File.binwrite("qr.png", qr.png(module_size: 8, background: nil)) # transparent
236
+
237
+ ean = Imprimatur.barcode(:ean13, "950600013435")
238
+ ean.text # => "9506000134352", the check digit added (or checked when given)
239
+
240
+ label = Imprimatur.barcode(:code128, "(00)106141412345678908", gs1: true) # GS1-128
241
+ dpp = Imprimatur.barcode(:datamatrix, "(01)09506000134352(21)ABC123", gs1: true)
242
+
243
+ Imprimatur.pdf("label.pdf", page_size: [300, 200], margin: 10) do |doc|
244
+ doc.barcode label, width: 280
245
+ doc.barcode dpp, width: 72, align: :right
246
+ end
247
+ ```
248
+
249
+ - `:qr` (`level:` `:l`, `:m`, `:q` or `:h`), `:datamatrix` (square),
250
+ `:ean13`, `:ean8`, `:upca` and `:code128`; `gs1: true` for the GS1 forms of
251
+ `:qr`, `:datamatrix` and `:code128`.
252
+ - GS1 element strings are written as people write them, AIs in parentheses,
253
+ and checked: AI syntax, GS1's character set, the AIs whose lengths are
254
+ predefined, and the check digits of SSCCs, GTINs and GLNs. Separators go in
255
+ only where a variable-length field needs one.
256
+ - `svg` and `png` take `module_size:`, `color:`, `background:` (nil for
257
+ none), `quiet_zone:` and, for linear codes, `bar_height:` in modules. PNGs
258
+ are one bit a pixel.
259
+ - In documents, barcodes are vector shapes in a PDF and a 300 dpi picture in
260
+ DOCX, at `width:` or `height:` points or else at their usual module size
261
+ (0.33 mm for EAN and UPC, 0.495 mm for GS1-128, 0.7 mm for QR Code and Data
262
+ Matrix), quiet zone included.
263
+ - `text`, `symbology`, `gs1?`, `width`, `height` (in modules), and `modules`
264
+ (rows of booleans) for drawing them some other way.
265
+
226
266
  ## Reading: documents as Markdown
227
267
 
228
268
  The other direction: a PDF, a workbook (XLSX, legacy XLS, ODS) or a DOCX
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "imprimatur-cli"
3
- version = "0.3.0"
3
+ version = "0.4.0"
4
4
  description = "Read PDF, XLSX, XLS, ODS and DOCX documents as Markdown or JSON, search them, extract their images."
5
5
  keywords = ["pdf", "xlsx", "docx", "markdown", "cli"]
6
6
  categories = ["command-line-utilities", "text-processing"]
@@ -10,6 +10,8 @@ rust-version.workspace = true
10
10
  authors.workspace = true
11
11
  license.workspace = true
12
12
  repository.workspace = true
13
+ # Tests and their fixtures build nothing a published crate needs.
14
+ exclude = ["tests/**", "src/**/tests.rs"]
13
15
 
14
16
  [[bin]]
15
17
  name = "imprimatur"
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "imprimatur"
3
- version = "0.3.0"
3
+ version = "0.4.0"
4
4
  description = "XLSX, PDF and DOCX authoring, and PDF, XLSX, XLS, ODS and DOCX reading, with its own XML, zip and PDF code."
5
5
  keywords = ["xlsx", "excel", "pdf", "docx", "spreadsheet"]
6
6
  categories = ["encoding", "text-processing"]
@@ -10,7 +10,16 @@ rust-version.workspace = true
10
10
  authors.workspace = true
11
11
  license.workspace = true
12
12
  repository.workspace = true
13
- include = ["src/**/*.rs", "src/**/*.txt", "src/**/tests/*.pdf", "src/**/tests/*.xml", "src/**/tests/*.rels", "README.md", "LICENSE*"]
13
+ include = [
14
+ "src/**/*.rs",
15
+ "src/**/*.txt",
16
+ "README.md",
17
+ "LICENSE*",
18
+ # Tests and their fixtures build nothing a published crate needs.
19
+ "!src/**/tests.rs",
20
+ "!src/**/tests/**",
21
+ "!src/**/test_support.rs",
22
+ ]
14
23
 
15
24
  [lints]
16
25
  workspace = true
@@ -18,10 +27,12 @@ workspace = true
18
27
  [dependencies]
19
28
  aes = "0.9.3"
20
29
  crc32fast = "1.5.2"
30
+ datamatrix = "0.3.3"
21
31
  md-5 = "0.11.0"
22
32
  miniz_oxide = "0.9.1"
33
+ qrcode = { version = "0.14.1", default-features = false }
23
34
  read-fonts = { version = "0.45.1", optional = true }
24
- serde_json = { version = "1.0.154", optional = true }
35
+ serde_json = { workspace = true, optional = true }
25
36
  sha2 = "0.11.1"
26
37
  thiserror = "2.0.21"
27
38
  zip = { version = "9.0.3", default-features = false, features = ["deflate-flate2-zlib-rs"], optional = true }
@@ -39,5 +50,5 @@ fonts = ["dep:read-fonts"]
39
50
 
40
51
  [dev-dependencies]
41
52
  font-test-data = "0.10.0"
42
- tempfile = "3"
53
+ tempfile.workspace = true
43
54
  zip = { version = "9.0.3", default-features = false, features = ["deflate-flate2-zlib-rs"] }
@@ -50,6 +50,20 @@ sheet.add_validation("A2:A1000".parse()?, short)?;
50
50
  # Ok::<(), imprimatur::Error>(())
51
51
  ```
52
52
 
53
+ ```rust
54
+ use imprimatur::barcode::{Barcode, Level, Look};
55
+ use imprimatur::doc::{Document, Symbol};
56
+
57
+ let qr = Barcode::qr("https://id.gs1.org/01/09506000134352", Level::Medium)?;
58
+ let svg = qr.svg(4.0, &Look::default())?; // or png(scale, &look)
59
+ let label = Barcode::gs1_128("(00)106141412345678908")?; // GS1 element strings, checked
60
+ let mut doc = Document::new();
61
+ doc.barcode(Symbol::new(label).width(250.0)?); // vector shapes in PDF
62
+ doc.barcode(Barcode::ean13("950600013435")?); // the check digit, added
63
+ let pdf = doc.to_pdf()?;
64
+ # Ok::<(), imprimatur::Error>(())
65
+ ```
66
+
53
67
  Everything Excel would refuse is an `Error` before a byte is written: invalid
54
68
  or duplicate sheet names, positions beyond 1,048,576 rows or 16,384 columns,
55
69
  text over 32,767 characters, formulas over 8,192, NaN and infinity, font
@@ -101,12 +115,21 @@ With the `fonts` feature, `font::TrueTypeFont::from_bytes` loads borrowed
101
115
  static TrueType data and exposes Unicode glyph IDs, horizontal advances and
102
116
  raw `hhea` line metrics in font units. It checks the required tables and
103
117
  outline offsets, but is not a full font sanitizer. Collections, WOFF, CFF and
104
- variable fonts are not supported. This is a Rust font-data API only: document
105
- font selection, PDF embedding, shaping and bidirectional layout are not
106
- implemented by it; PDF writing still uses Helvetica.
118
+ variable fonts are not supported. This is a Rust font-data API only. The
119
+ internal PDF writer can serialize TrueType font resources with every glyph
120
+ and Unicode text mappings; document font selection, shaping and bidirectional
121
+ layout still need to connect to them. Document PDF output uses Helvetica.
122
+
123
+ Barcodes: QR Code and Data Matrix (their GS1 forms too), EAN-13, EAN-8, UPC-A,
124
+ Code 128 and GS1-128, drawn as SVG, as PNG, or into a document (shapes in a
125
+ PDF, a 300 dpi picture in DOCX). GS1 element strings are checked: AI syntax,
126
+ GS1's character set, predefined lengths, and the check digits of SSCCs, GTINs
127
+ and GLNs.
107
128
 
108
129
  Dependencies: `miniz_oxide`, `crc32fast` and `thiserror`; reading encrypted
109
- PDFs adds `md-5`, `sha2` and `aes`.
130
+ PDFs adds `md-5`, `sha2` and `aes`; barcodes add `qrcode` and `datamatrix`
131
+ (for their error correction and placement; the linear codes, GS1 and the
132
+ drawing are ours).
110
133
 
111
134
  Three features add what the core does not need: `json` (`read::json`, the
112
135
  schema the command line and the Ruby gem print, through `serde_json`),
@@ -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
+ }