swiss_qr 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8277ee2838f01d43a46c5548d3398f06d2ff9a035d65b5063d3be912258c5d24
4
+ data.tar.gz: 5f594ffdbf468ed9904e671f87c5a7bf859e158858c51da381e215ce39289035
5
+ SHA512:
6
+ metadata.gz: 21908cccd4846e92b36747f59f4bcc0579dc90deb254802f1e34ca5170d564bf1be923de4a3f353c128043e01ae333a4b1a4232ad0819cb5994ee911c6b2a9b6
7
+ data.tar.gz: 3d417aea462376fb6dca46037f72be203c3d628d76dd3245b02df3dcc66f9dc0c32cdf7be310f6fa93ca261d877b3e2dc7066bcc6da82eb23c93ff46766ef22a
data/CHANGELOG.md ADDED
@@ -0,0 +1,27 @@
1
+ # Changelog
2
+
3
+ ## [Unreleased]
4
+
5
+ ## [0.1.0] - 2026-10-16
6
+
7
+ First release.
8
+
9
+ - `SwissQr.parse(payload, strict: true)` parses the payload of the Swiss QR
10
+ Code on a Swiss QR-bill (SIX Implementation Guidelines 2.3/2.4, `SPC` /
11
+ `0200`) into immutable `Data` objects: `Bill`, `Address` and
12
+ `BillingInformation` (Swico S1). `SwissQr.valid?` and
13
+ `SwissQr.looks_like_qr_bill?` go with it.
14
+ - Every rule of the guidelines is validated: IBAN and QR-IBAN check digits,
15
+ reference type against account, QR reference (mod 10 recursive), ISO 11649
16
+ creditor reference, amount, currency, addresses, field lengths, character
17
+ set, the 997-character limit and line separators. Problems come back per
18
+ field as errors and warnings; `strict: false` returns the bill with them.
19
+ Deviations real generators make (CR LF or LF, a final line break, trailing
20
+ empty lines, a byte order mark) are accepted.
21
+ - `SwissQr.read` and `SwissQr.read_all` find QR-bills in images and PDFs
22
+ through a pluggable reader: qr_reader 0.0.3 or newer by default (optional,
23
+ loaded on first use), or any object with `decode_all` or any callable. A
24
+ `String` input is always a file path; image bytes come as an IO.
25
+ - `swiss_qr` command: prints the bills in images, PDFs or a payload as JSON.
26
+ - Runs on MRI 3.3 to 4.0, JRuby 10 and TruffleRuby (tested in CI), and on
27
+ ruby.wasm with bigdecimal-pure.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Andi Idogawa
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,402 @@
1
+ # SwissQr
2
+
3
+ Parses and validates the Swiss QR Code printed on a Swiss QR-bill
4
+ ("QR-Rechnung", "QR-facture", "QR-fattura"), as defined by the SIX *Swiss
5
+ Implementation Guidelines for the QR-bill* (QR type `SPC`, version `0200`,
6
+ coding type `1`, trailer `EPD`). It can also find the code in images and PDFs.
7
+
8
+ - **Parser with no dependencies** other than `bigdecimal`. Values come back as
9
+ immutable `Data` objects: `SwissQr::Bill`, `SwissQr::Address` and
10
+ `SwissQr::BillingInformation` (Swico/SIX "S1" billing information).
11
+ - **Checks every payload rule:** header, IBAN and QR-IBAN check digits, the
12
+ reference type against the account, QR reference (modulo 10 recursive), ISO
13
+ 11649 creditor reference (modulo 97), amount, currency, addresses, the
14
+ reserved ultimate creditor, field lengths, character set, the 997-character
15
+ limit and line separators.
16
+ - **Tolerant where real generators deviate:** CR LF or LF, a final line break
17
+ (of either kind), trailing empty lines, a byte order mark and blanks after
18
+ free text are accepted (SIX's own samples have some of these).
19
+ - **Reads QR codes from images and PDFs** through a pluggable reader. By
20
+ default this is the pure-Ruby [`qr_reader`](https://rubygems.org/gems/qr_reader)
21
+ gem, which is optional and loaded only when you call `SwissQr.read`. Any
22
+ other decoder (ZXing, ZBar, a cloud API, ...) can be plugged in instead.
23
+ - **`swiss_qr` command** that prints bills as JSON.
24
+ - **Runs on every major Ruby implementation:** MRI (CRuby) 3.3 to 4.0, JRuby 10,
25
+ TruffleRuby and ruby.wasm (with
26
+ [`bigdecimal-pure`](https://rubygems.org/gems/bigdecimal-pure)). All of them
27
+ run the test suite in CI except ruby.wasm, which was tested by hand; see
28
+ [Ruby versions and platforms](#ruby-versions-and-platforms).
29
+
30
+ ## Installation
31
+
32
+ ```bash
33
+ bundle add swiss_qr
34
+ bundle add qr_reader # optional: only needed for SwissQr.read / read_all and the CLI
35
+ ```
36
+
37
+ or `gem install swiss_qr qr_reader`.
38
+
39
+ ### Ruby versions and platforms
40
+
41
+ Ruby 3.3 or newer. Both gems are plain Ruby; neither has a C extension of its
42
+ own.
43
+
44
+ | Ruby | |
45
+ |---|---|
46
+ | MRI 3.3, 3.4, 4.0 | tested |
47
+ | TruffleRuby 25 | tested |
48
+ | JRuby 10 | tested |
49
+ | ruby.wasm (Ruby 4.0) | works with `bigdecimal-pure`, see below |
50
+ | MRI 3.2 and older, JRuby 9.4 and older | not supported (3.2 is end of life) |
51
+
52
+ Amounts, VAT rates and discounts are `BigDecimal`s, so swiss_qr depends on
53
+ the `bigdecimal` gem. That gem is a C extension. Where it cannot be built or
54
+ loaded, ruby.wasm above all, use
55
+ [`bigdecimal-pure`](https://rubygems.org/gems/bigdecimal-pure) instead: a
56
+ `BigDecimal` written in Ruby on top of `Rational`, exact like the original for
57
+ everything swiss_qr does. Its `require "bigdecimal"` loads the native
58
+ extension when there is one, and the pure class otherwise.
59
+
60
+ Put its `lib` on the load path ahead of swiss_qr, which is how gems are
61
+ usually loaded into ruby.wasm (for example with the `@ruby/4.0-wasm-wasi`
62
+ npm package):
63
+
64
+ ```ruby
65
+ $LOAD_PATH.unshift "/gems/bigdecimal-pure/lib", "/gems/swiss_qr/lib", "/gems/qr_reader/lib"
66
+ require "swiss_qr" # its require "bigdecimal" gets bigdecimal-pure's class
67
+ ```
68
+
69
+ Reading images and PDFs needs qr_reader 0.0.3 or newer (`SwissQr.read`
70
+ raises `SwissQr::ReaderUnavailable` with an older one). Parsing payloads does
71
+ not use qr_reader at all.
72
+
73
+ Under Bundler this does not work yet: swiss_qr's dependency installs the
74
+ `bigdecimal` gem itself (Bundler cannot substitute one gem for another), and
75
+ bigdecimal-pure 0.1.0 does not fall back to its pure class when that gem's
76
+ Ruby files are present but its C extension cannot be loaded.
77
+
78
+ ## Parsing a payload
79
+
80
+ ```ruby
81
+ require "swiss_qr"
82
+
83
+ bill = SwissQr.parse(payload) # raises SwissQr::InvalidPayload on any rule violation
84
+ bill.iban # => "CH4431999123000889012"
85
+ bill.qr_iban? # => true
86
+ bill.formatted_iban # => "CH44 3199 9123 0008 8901 2"
87
+ bill.amount # => 0.194975e4 (BigDecimal, nil when open)
88
+ bill.formatted_amount # => "1949.75"
89
+ bill.currency # => "CHF"
90
+ bill.creditor.name # => "Max Muster & Söhne"
91
+ bill.creditor.lines # => ["Max Muster & Söhne", "Musterstrasse 123", "8000 Seldwyla"]
92
+ bill.debtor&.town # => "Seldwyla"
93
+ bill.reference_type # => :qrr (:qrr, :scor or :non)
94
+ bill.reference # => "210000000003139471430009017"
95
+ bill.formatted_reference # => "21 00000 00003 13947 14300 09017"
96
+ bill.unstructured_message # => "Order from 15.10.2020"
97
+ bill.billing_information&.invoice_number # => "1234"
98
+ bill.billing_information&.invoice_date # => Date.new(2020, 10, 21)
99
+ bill.billing_information&.payment_conditions # => [#<data PaymentCondition discount=0.0, days=30>]
100
+ bill.alternative_procedures # => ["Name AV1: UV;UltraPay005;12345", "Name AV2: XY;XYService;54321"]
101
+ bill.warnings # => [] (e.g. legacy address type "K")
102
+ bill.to_h # Ruby types (BigDecimal, Date, Symbol), nested hashes
103
+ bill.to_json # JSON (amount as "1949.75", symbols as strings)
104
+ ```
105
+
106
+ The payload is the decoded text of the QR code: a String, ideally UTF-8.
107
+ Binary strings are taken as UTF-8, and strings in other encodings (UTF-16,
108
+ UTF-32, real Latin-1, ...) are transcoded. A string tagged ISO-8859-1 or
109
+ Windows-1252 whose bytes are valid UTF-8 is read as UTF-8 with a warning:
110
+ many QR decoders tag byte-mode data that way, but a QR-bill is always UTF-8.
111
+ Input longer than 4096 bytes (more than any QR code holds) is rejected with
112
+ `InvalidPayload` before it is parsed.
113
+
114
+ ```ruby
115
+ SwissQr.valid?(payload) # => true / false, never raises
116
+ SwissQr.looks_like_qr_bill?(text) # cheap "starts like SPC/0200" check, no validation
117
+ ```
118
+
119
+ ## Reading QR-bills from images and PDFs
120
+
121
+ ```ruby
122
+ require "swiss_qr"
123
+
124
+ bill = SwissQr.read("invoice.pdf") # first QR-bill found, or nil
125
+ bills = SwissQr.read_all("scans.pdf") # every QR-bill on every page, [] when none
126
+ ```
127
+
128
+ `input` can be:
129
+
130
+ - a file path (`String` or `Pathname`),
131
+ - an `IO` with the image or PDF: a `File`, an upload's `Tempfile`, or
132
+ `StringIO.new(bytes)` for bytes you already hold,
133
+ - with the default reader, also a `ChunkyPNG::Image`-like object (`#width`,
134
+ `#height`, `#pixels`).
135
+
136
+ A `String` is always a file path, never image data, as with `File.read`. Image
137
+ bytes in a `String` raise `ArgumentError` (wrap them in `StringIO.new`), so
138
+ bytes uploaded by a client can never be taken for a path on the server:
139
+
140
+ ```ruby
141
+ SwissQr.read(params[:invoice].tempfile) # an upload
142
+ SwissQr.read(StringIO.new(pdf_bytes)) # bytes from an API or a database
143
+ ```
144
+
145
+ Supported formats are PNG, JPEG, JPEG 2000, GIF, BMP, PNM and TIFF (including
146
+ fax), and PDFs (scanned pages, as well as vector-drawn codes in generated
147
+ PDFs). The Swiss cross in the middle of the code is no problem.
148
+
149
+ Other QR codes on the page, such as a link to an online portal or a TWINT code,
150
+ are skipped: only texts for which `SwissQr.looks_like_qr_bill?` is true are parsed. `read`
151
+ stops at the first QR-bill. With the default reader it does not decode the
152
+ rest of a long PDF.
153
+
154
+ `strict:` works as for `parse` (see below). With the default `strict: true`, an
155
+ invalid QR-bill in the image raises `SwissQr::InvalidPayload`. A QR code that
156
+ does not start with `SPC` is not a QR-bill, so it is skipped and does not
157
+ raise. Errors from the reader itself propagate: `Errno::ENOENT` for a missing
158
+ file, and `QrReader::ImageError` or `QrReader::UnsupportedImageError` for
159
+ unreadable data.
160
+
161
+ ### Choosing or replacing the QR code reader
162
+
163
+ `SwissQr` does not decode QR codes itself. It asks a *reader* for the texts of
164
+ all codes in the input. A reader is either:
165
+
166
+ - **an object with `decode_all(input)`** that returns an Array of Strings (the
167
+ `QrReader` module is one), or
168
+ - **a callable** (Proc, lambda, `Method`, any object with `#call(input)`) that
169
+ returns a String, an Array of Strings, or `nil`.
170
+
171
+ Elements may also be result objects with a `#text` method (for example
172
+ `QrReader::Result`). `nil` elements are ignored.
173
+
174
+ The reader gets the input as passed to `read`. A `String` it receives is
175
+ always a file path, because image bytes in a `String` are refused before any
176
+ reader is asked.
177
+
178
+ The default reader is `SwissQr::Reader::QrReaderAdapter`. Creating it does not
179
+ load anything. It runs `require "qr_reader"` on the first read. If the gem is
180
+ missing, the read raises `SwissQr::ReaderUnavailable`, and the message says how
181
+ to install the gem (`gem install qr_reader` or `bundle add qr_reader`) or to set
182
+ another reader. `require "swiss_qr"` never loads `qr_reader`.
183
+
184
+ Replace the reader for the whole process, or pass one per call:
185
+
186
+ ```ruby
187
+ # qr_reader with options (fail fast on pages without a code); options are
188
+ # passed on to qr_reader, which is still loaded only on the first read
189
+ SwissQr.reader = SwissQr::Reader::QrReaderAdapter.new(try_harder: false)
190
+
191
+ # any object with decode_all
192
+ class MyZxingReader
193
+ def decode_all(path) = ZXing.decode_all(path) # e.g. the zxing gem (JRuby)
194
+ end
195
+ SwissQr.reader = MyZxingReader.new
196
+
197
+ # ZBar's command-line tool, for images that carry a single code
198
+ # (zbarimg --raw cannot separate several multi-line codes)
199
+ require "open3"
200
+ SwissQr.reader = lambda do |path|
201
+ out, status = Open3.capture2("zbarimg", "--quiet", "--raw", "-Sdisable", "-Sqrcode.enable", path.to_s)
202
+ status.success? ? out.chomp : nil
203
+ end
204
+
205
+ # per call; the global reader is unchanged
206
+ SwissQr.read("scan.png", reader: ->(path) { my_cloud_ocr(path) })
207
+
208
+ SwissQr.reader = nil # back to the default (qr_reader)
209
+ ```
210
+
211
+ Prefer `QrReaderAdapter.new(**options)` over `SwissQr.reader = QrReader` or a
212
+ lambda calling `QrReader.decode_all` (which also needs `require "qr_reader"`):
213
+ only the adapter returns no codes for a PDF without any image, where qr_reader
214
+ itself raises `QrReader::UnsupportedImageError`, and only the adapter lets
215
+ `read` stop decoding at the first QR-bill.
216
+
217
+ A callable may return a single result object (anything with `#text`, such as a
218
+ `Struct` or `Data`) instead of an Array; it counts as one code.
219
+
220
+ `SwissQr.reader=` checks that the object responds to `#decode_all` or `#call`
221
+ and raises `ArgumentError` otherwise. A reader that returns anything other
222
+ than Strings (or objects with `#text`) makes `read` raise `TypeError`.
223
+
224
+ Assigning `SwissQr.reader` is thread-safe, but the reader is global to the
225
+ process. Assign it once at boot, or pass `reader:` per call.
226
+
227
+ ## Validation and strict mode
228
+
229
+ ```ruby
230
+ SwissQr.parse(payload) # strict: true (default) raises on any error
231
+ bill = SwissQr.parse(payload, strict: false)
232
+ bill.valid? # => false
233
+ bill.errors.map(&:to_s) # => ["reference: QR reference check digit (modulo 10 recursive) is wrong: ..."]
234
+ bill.warnings # problems that do not make the bill invalid
235
+ ```
236
+
237
+ - **`strict: true`** raises `SwissQr::InvalidPayload` when there is any error.
238
+ `#errors` lists every `SwissQr::Problem`, and `#bill` holds the partially
239
+ parsed bill.
240
+ - **`strict: false`** returns the bill with `errors` filled in. It still raises
241
+ `InvalidPayload` when the text is not structurally a QR-bill: the QR type is
242
+ not `SPC`, or there are fewer than 31 lines. `#bill` is `nil` then.
243
+ - A **`SwissQr::Problem`** has a `field` symbol (`:iban`, `:"creditor.name"`,
244
+ `:reference`, `:payload`, ...) and a `message`. `problem.about?(:creditor)`
245
+ matches all creditor fields.
246
+ - **Warnings** never make a bill invalid. Billing information and alternative
247
+ procedures have status "A" in the guidelines (errors there must not lead to
248
+ rejection), so only warnings are given for them. Other warnings: legacy
249
+ address type `K`, a removed BOM, blanks around free text (names, street,
250
+ town, message), a UTF-8 payload tagged as Latin-1, a postal code that may
251
+ have a country prefix. Blanks around fixed-format elements (IBAN, amount,
252
+ codes, reference, country) are errors.
253
+
254
+ Errors: `SwissQr::Error` (base) → `SwissQr::InvalidPayload`,
255
+ `SwissQr::ReaderUnavailable`.
256
+
257
+ ## Value objects
258
+
259
+ All three are immutable (`Data`) and support `#to_h`, `#as_json` and `==`.
260
+
261
+ ### `SwissQr::Bill`
262
+
263
+ | Field | Type | Notes |
264
+ |---|---|---|
265
+ | `version` | String | `"0200"` |
266
+ | `coding_type` | String | `"1"` (UTF-8, Latin character set) |
267
+ | `iban` | String | IBAN or QR-IBAN, no spaces, upper case (`formatted_iban` adds spaces) |
268
+ | `qr_iban` | Boolean | Also `qr_iban?`. QR-IID 30000–31999 |
269
+ | `creditor` | Address | Required |
270
+ | `ultimate_creditor` | Address, nil | Reserved by SIX. Must be empty, so always `nil` in a valid bill |
271
+ | `amount` | BigDecimal, nil | `nil` = open amount (`open_amount?`). `0.00` = notification (`notification?`). `formatted_amount` gives `"50.00"` |
272
+ | `currency` | String | `"CHF"` or `"EUR"` |
273
+ | `debtor` | Address, nil | Optional |
274
+ | `reference_type` | Symbol | `:qrr` (QR reference, needs QR-IBAN), `:scor` (ISO 11649 creditor reference) or `:non` |
275
+ | `reference` | String, nil | As given. `formatted_reference` groups the digits |
276
+ | `unstructured_message` | String, nil | Free text |
277
+ | `trailer` | String | `"EPD"` |
278
+ | `raw_billing_information` | String, nil | Billing information as given (line 32) |
279
+ | `billing_information` | BillingInformation, nil | The billing information parsed, when it uses the Swico S1 syntax (`//S1/...`) |
280
+ | `alternative_procedures` | Array of String | At most 2 (e.g. `"eBill/B/..."`) |
281
+ | `errors`, `warnings` | Array of Problem | `valid?` is `errors.empty?` |
282
+ | `raw` | String | The payload as given (UTF-8) |
283
+
284
+ ### `SwissQr::Address`
285
+
286
+ | Field | Notes |
287
+ |---|---|
288
+ | `type` | `:structured` (`S`) or `:combined` (`K`, legacy). Also `structured?` and `combined?` |
289
+ | `name` | Name or company |
290
+ | `street`, `building_number` | Type `S` only |
291
+ | `postal_code`, `town` | Type `S` only |
292
+ | `address_line1`, `address_line2` | Type `K` only (street and number, then postal code and town) |
293
+ | `country` | ISO 3166-1 alpha-2, e.g. `"CH"` |
294
+
295
+ `#lines` gives the postal lines for display. `#to_s` joins them with line
296
+ breaks.
297
+
298
+ ### `SwissQr::BillingInformation` (Swico S1)
299
+
300
+ | Field | S1 tag | Type |
301
+ |---|---|---|
302
+ | `invoice_number` | `/10/` | String |
303
+ | `invoice_date` | `/11/` | Date |
304
+ | `customer_reference` | `/20/` | String |
305
+ | `vat_number` | `/30/` | String (UID digits without `CHE`) |
306
+ | `vat_dates` | `/31/` | Array of Date (one date, or start and end of a period) |
307
+ | `vat_details` | `/32/` | Array of `VatRate(rate, amount)` |
308
+ | `vat_import_taxes` | `/33/` | Array of `VatRate(rate, amount)` |
309
+ | `payment_conditions` | `/40/` | Array of `PaymentCondition(discount, days)` |
310
+ | `unknown_tags` | other tags | Hash |
311
+ | `warnings`, `raw` | | |
312
+
313
+ `SwissQr::BillingInformation.parse("//S1/10/10201409/11/190512/32/7.7/40/2:10;0:30")`
314
+ parses a string directly.
315
+
316
+ ### Helpers
317
+
318
+ ```ruby
319
+ SwissQr::Iban.valid?("CH44 3199 9123 0008 8901 2") # => true
320
+ SwissQr::Iban.qr_iban?("CH4431999123000889012") # => true
321
+ SwissQr::Iban.format("CH4431999123000889012") # => "CH44 3199 9123 0008 8901 2"
322
+ SwissQr::Reference.valid_qrr?("210000000003139471430009017") # => true
323
+ SwissQr::Reference.valid_scor?("RF18 5390 0754 7034") # => true
324
+ SwissQr::Reference.build_qrr("21000000000313947143000901") # => "210000000003139471430009017" (adds the check digit)
325
+ SwissQr::Reference.build_scor("539007547034") # => "RF18539007547034"
326
+ SwissQr::Reference.format_qrr("210000000003139471430009017") # => "21 00000 00003 13947 14300 09017"
327
+ SwissQr::Reference.format_scor("RF18539007547034") # => "RF18 5390 0754 7034"
328
+ ```
329
+
330
+ ## Supported specification
331
+
332
+ - **Swiss Implementation Guidelines for the QR-bill, version 2.3** (in force
333
+ since November 2025). The payload version is `0200`.
334
+ - **Version 2.4** (in force from 14 November 2026) does not change the payload
335
+ version, so the two cannot be told apart from the payload. Its main new rule
336
+ (QR-IBAN with a QR reference only in CHF) is reported as a warning on
337
+ `currency`.
338
+ - **Version 2.2 and older:** address type `K` (combined address), removed in
339
+ 2.3, is still parsed (`address_line1`, `address_line2`) with a warning.
340
+ - **Billing information:** the SIX/Swico "S1" syntax (IG Annex D).
341
+ - **Not yet published:** master version `03`, which is when the payload version
342
+ string will start to change. It is not supported.
343
+
344
+ `docs/SPEC_NOTES.md` digests the SIX documents (IG 2.2–2.4, processing rules,
345
+ QR-IID, S1 syntax), with a source for every rule. The test fixtures in
346
+ `test/fixtures` include the payloads of the official SIX sample QR codes.
347
+
348
+ ## Command line
349
+
350
+ ```console
351
+ $ swiss_qr invoice.pdf scan.png # all QR-bills in the files, as a JSON array
352
+ [
353
+ {
354
+ "file": "invoice.pdf",
355
+ "version": "0200",
356
+ "iban": "CH4431999123000889012",
357
+ "amount": "1949.75",
358
+ ...
359
+ }
360
+ ]
361
+ $ swiss_qr --text < payload.txt # parse a payload (the QR code's text) from stdin
362
+ $ swiss_qr - < scan.png # image from stdin
363
+ $ swiss_qr --lines *.pdf # one compact JSON object per line
364
+ $ swiss_qr --no-strict broken.png # print invalid bills too, with their "errors"
365
+ $ swiss_qr --version
366
+ ```
367
+
368
+ Each JSON object is `Bill#as_json` plus `"file"` (`"-"` for stdin). Messages
369
+ go to stderr.
370
+
371
+ | Exit status | Meaning |
372
+ |---|---|
373
+ | 0 | Every input held at least one QR-bill, and all of them were valid |
374
+ | 1 | An input held no QR-bill, or held an invalid one. With `--no-strict`, invalid bills are still printed |
375
+ | 2 | Usage error, unreadable file, or no QR code reader (`gem install qr_reader`) |
376
+
377
+ The command uses `SwissQr.reader`, so it needs `qr_reader` to be installed.
378
+
379
+ ## Development
380
+
381
+ After checking out the repo, run `bin/setup` to install dependencies (this
382
+ includes `qr_reader`, `rqrcode` and `chunky_png` for the image tests). Then run
383
+ `bundle exec rake` for the tests and RuboCop. `bin/console` gives an
384
+ interactive prompt. `test/fixtures/README.md` explains how the fixtures are
385
+ generated.
386
+
387
+ ### Releasing
388
+
389
+ Releases are published from GitHub Actions with RubyGems
390
+ [trusted publishing](https://guides.rubygems.org/trusted-publishing/) (no API
391
+ key is stored anywhere), and the gem carries a Sigstore attestation, so
392
+ RubyGems.org shows which repository, workflow and commit built it. Bump
393
+ `SwissQr::VERSION`, update `CHANGELOG.md`, then:
394
+
395
+ ```bash
396
+ git tag v0.1.0
397
+ git push origin v0.1.0
398
+ ```
399
+
400
+ ## License
401
+
402
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[test rubocop]