Skip to main content

pdf_xfa/
barcode.rs

1// Copyright (c) 2026 Innovation Trigger B.V.
2//
3// PDFluent is available under two licences, at your option: the GNU AGPLv3, or
4// the PDFluent Commercial Licence. See the LICENSE file in this repository --
5// that file travels with the copy you received, which a URL does not.
6
7//! Barcodes for XFA form fields.
8//!
9//! A `<field>` carrying `<ui><barcode type="..."/></ui>` was recognised, placed
10//! and sized, and then drawn as ordinary text -- there was no encoder. The form
11//! is then not scannable, which is the one thing a barcode is on it for. See
12//! #147.
13//!
14//! WAT HIER WEL EN NIET IN ZIT
15//!
16//! Code 39 (`code39`, ook wel Code 3 of 9). Gekozen omdat het het meest
17//! voorkomt in overheids- en industriële formulieren, en omdat het zonder
18//! tabellen van derden correct te implementeren is: elk teken is negen
19//! elementen -- vijf balken en vier tussenruimtes -- waarvan er precies drie
20//! breed zijn.
21//!
22//! Andere typen (Code 128, EAN-13, PDF417, QR) volgen niet vanzelf: Code 128
23//! heeft codesets en een checksum, PDF417 en QR zijn tweedimensionaal en vragen
24//! foutcorrectie. Die staan als grens vastgelegd in plaats van half gebouwd --
25//! een barcode die er goed uitziet en niet scant, is erger dan geen barcode.
26
27/// Welke barcodesoorten we kunnen tekenen.
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum BarcodeType {
30    /// Code 39, met de `*`-start- en stoptekens die scanners verwachten.
31    Code39,
32}
33
34impl BarcodeType {
35    /// Herken het `type`-attribuut uit `<barcode type="...">`.
36    ///
37    /// XFA schrijft de namen in kleine letters zonder scheidingsteken
38    /// (`code39`, `code128`, `ean13`). Onbekende of nog niet ondersteunde
39    /// soorten geven `None`; de aanroeper valt dan terug op tekst, wat
40    /// zichtbaar verkeerd is en daarmee eerlijker dan een balkenpatroon dat
41    /// niets betekent.
42    pub fn from_xfa_name(naam: &str) -> Option<Self> {
43        match naam.trim().to_ascii_lowercase().as_str() {
44            "code39" | "code3of9" => Some(Self::Code39),
45            _ => None,
46        }
47    }
48}
49
50/// De Code 39-alfabet: teken -> negen elementen, `true` is breed.
51///
52/// Volgorde is balk, ruimte, balk, ruimte, ... beginnend en eindigend met een
53/// balk. Precies drie van de negen zijn breed; dat is de controle die de test
54/// hieronder op de hele tabel uitvoert.
55const CODE39: &[(char, [bool; 9])] = &[
56    (
57        '0',
58        [false, false, false, true, true, false, true, false, false],
59    ),
60    (
61        '1',
62        [true, false, false, true, false, false, false, false, true],
63    ),
64    (
65        '2',
66        [false, false, true, true, false, false, false, false, true],
67    ),
68    (
69        '3',
70        [true, false, true, true, false, false, false, false, false],
71    ),
72    (
73        '4',
74        [false, false, false, true, true, false, false, false, true],
75    ),
76    (
77        '5',
78        [true, false, false, true, true, false, false, false, false],
79    ),
80    (
81        '6',
82        [false, false, true, true, true, false, false, false, false],
83    ),
84    (
85        '7',
86        [false, false, false, true, false, false, true, false, true],
87    ),
88    (
89        '8',
90        [true, false, false, true, false, false, true, false, false],
91    ),
92    (
93        '9',
94        [false, false, true, true, false, false, true, false, false],
95    ),
96    (
97        'A',
98        [true, false, false, false, false, true, false, false, true],
99    ),
100    (
101        'B',
102        [false, false, true, false, false, true, false, false, true],
103    ),
104    (
105        'C',
106        [true, false, true, false, false, true, false, false, false],
107    ),
108    (
109        'D',
110        [false, false, false, false, true, true, false, false, true],
111    ),
112    (
113        'E',
114        [true, false, false, false, true, true, false, false, false],
115    ),
116    (
117        'F',
118        [false, false, true, false, true, true, false, false, false],
119    ),
120    (
121        'G',
122        [false, false, false, false, false, true, true, false, true],
123    ),
124    (
125        'H',
126        [true, false, false, false, false, true, true, false, false],
127    ),
128    (
129        'I',
130        [false, false, true, false, false, true, true, false, false],
131    ),
132    (
133        'J',
134        [false, false, false, false, true, true, true, false, false],
135    ),
136    (
137        'K',
138        [true, false, false, false, false, false, false, true, true],
139    ),
140    (
141        'L',
142        [false, false, true, false, false, false, false, true, true],
143    ),
144    (
145        'M',
146        [true, false, true, false, false, false, false, true, false],
147    ),
148    (
149        'N',
150        [false, false, false, false, true, false, false, true, true],
151    ),
152    (
153        'O',
154        [true, false, false, false, true, false, false, true, false],
155    ),
156    (
157        'P',
158        [false, false, true, false, true, false, false, true, false],
159    ),
160    (
161        'Q',
162        [false, false, false, false, false, false, true, true, true],
163    ),
164    (
165        'R',
166        [true, false, false, false, false, false, true, true, false],
167    ),
168    (
169        'S',
170        [false, false, true, false, false, false, true, true, false],
171    ),
172    (
173        'T',
174        [false, false, false, false, true, false, true, true, false],
175    ),
176    (
177        'U',
178        [true, true, false, false, false, false, false, false, true],
179    ),
180    (
181        'V',
182        [false, true, true, false, false, false, false, false, true],
183    ),
184    (
185        'W',
186        [true, true, true, false, false, false, false, false, false],
187    ),
188    (
189        'X',
190        [false, true, false, false, true, false, false, false, true],
191    ),
192    (
193        'Y',
194        [true, true, false, false, true, false, false, false, false],
195    ),
196    (
197        'Z',
198        [false, true, true, false, true, false, false, false, false],
199    ),
200    (
201        '-',
202        [false, true, false, false, false, false, true, false, true],
203    ),
204    (
205        '.',
206        [true, true, false, false, false, false, true, false, false],
207    ),
208    (
209        ' ',
210        [false, true, true, false, false, false, true, false, false],
211    ),
212    (
213        '$',
214        [false, true, false, true, false, true, false, false, false],
215    ),
216    (
217        '/',
218        [false, true, false, true, false, false, false, true, false],
219    ),
220    (
221        '+',
222        [false, true, false, false, false, true, false, true, false],
223    ),
224    (
225        '%',
226        [false, false, false, true, false, true, false, true, false],
227    ),
228    (
229        '*',
230        [false, true, false, false, true, false, true, false, false],
231    ),
232];
233
234fn patroon(c: char) -> Option<[bool; 9]> {
235    let boven = c.to_ascii_uppercase();
236    CODE39.iter().find(|(k, _)| *k == boven).map(|(_, p)| *p)
237}
238
239/// Eén element uit een gecodeerde barcode.
240#[derive(Debug, Clone, Copy, PartialEq)]
241pub struct Element {
242    /// `true` voor een balk, `false` voor een tussenruimte.
243    pub is_bar: bool,
244    /// Breedte in modules: 1 voor smal, 3 voor breed.
245    pub modules: u8,
246}
247
248/// Codeer `tekst` als Code 39.
249///
250/// Geeft `None` als er een teken in staat dat Code 39 niet kent. Dat is bewust
251/// geen stille overslag: een barcode waar tekens uit weggelaten zijn, scant
252/// naar iets anders dan er staat, en dat is erger dan niets tekenen.
253pub fn encode_code39(tekst: &str) -> Option<Vec<Element>> {
254    // Start- en stopteken horen erbij; zonder herkent een scanner de code niet.
255    let met_bookends: String = format!("*{}*", tekst.to_ascii_uppercase());
256    let mut uit = Vec::new();
257    for (i, c) in met_bookends.chars().enumerate() {
258        if c == '*' && i != 0 && i != met_bookends.chars().count() - 1 {
259            // Een `*` middenin zou de code vroegtijdig afsluiten.
260            return None;
261        }
262        let p = patroon(c)?;
263        for (j, breed) in p.iter().enumerate() {
264            uit.push(Element {
265                is_bar: j % 2 == 0,
266                modules: if *breed { 3 } else { 1 },
267            });
268        }
269        // Tussenruimte tussen tekens, altijd smal.
270        uit.push(Element {
271            is_bar: false,
272            modules: 1,
273        });
274    }
275    uit.pop(); // geen losse ruimte achteraan
276    Some(uit)
277}
278
279/// Totale breedte in modules, zodat de aanroeper kan schalen naar de veldbreedte.
280pub fn total_modules(elementen: &[Element]) -> u32 {
281    elementen.iter().map(|e| u32::from(e.modules)).sum()
282}