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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4b0c00d27618298889f1419aec02a69a9de179fb9f3caaa5f33e57db05a68a9b
|
|
4
|
+
data.tar.gz: f8c77e28a3f3ada14611260a7f1ffffc830ba3f6e0ec6d0675b4c8177b391f72
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
+
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
|
+
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 = [
|
|
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 = {
|
|
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 =
|
|
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
|
|
105
|
-
|
|
106
|
-
|
|
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
|
+
}
|