hexapdf-extras 1.0.1 → 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: be80955331e2df5f0ce6c549d335930bf5eed3e031deb166e6b13122bfca89d1
4
- data.tar.gz: ba2c0f0affc9be014a6e87a4bbdb08bd8843af6a179fc0cd2acb548191eba47e
3
+ metadata.gz: 8e7fe626cf7fd851cd203b739c69bdd193b5debffcd129c656d59b51c2d326ad
4
+ data.tar.gz: 588a1a744477c437035ca66fd187f4c3563b135946efb8a21d5f73e77f15d238
5
5
  SHA512:
6
- metadata.gz: 60b8b6103c054737c98ef349e5cf7f5f6596eec6bbfdb23ef4dbd04623ecdbf4d1076332be273b19ff10d6931543ab30e8db806fdbfa317e1ab5619d974028c8
7
- data.tar.gz: e4e5144d3b851225a971e5944d160fe6a7d4f8dd32bba2e89e0dd3dd6e30cdc52bfa95e2cd3d9a8a36b8761c506a9368817775f57bbe06130ad2d809f1f9ae8d
6
+ metadata.gz: fb195eef642905d2797b96c545f63eaee7edff117982e16e8c3965d05c9abdf537331d35cfa09df6d9aebac3d03e4a28dbb921714bc3a4968984789f2f4548ea
7
+ data.tar.gz: 8682207937162bef5c86ca5f0f061dbb25469b989e65fa51dc4fbe7f3bc75739cc382911efee4c954e30a14650322bd870aa308e56436a1e18a665e0f4174a9d
data/Rakefile CHANGED
@@ -27,7 +27,7 @@ task publish_files: [:package] do
27
27
  end
28
28
 
29
29
  task :test_all do
30
- versions = `rbenv versions --bare | grep -i 2.[67]\\\\\\|3.`.split("\n")
30
+ versions = `rbenv versions --bare | grep -i 2.[7]\\\\\\|3.`.split("\n")
31
31
  versions.each do |version|
32
32
  sh "eval \"$(rbenv init -)\"; rbenv shell #{version} && ruby -v && rake test"
33
33
  end
@@ -0,0 +1,656 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'hexapdf/error'
4
+ require 'hexapdf/layout/box'
5
+ require 'hexapdf/extras/layout'
6
+
7
+ module HexaPDF::Extras::Layout::NumericMeasurementHelper #:nodoc:
8
+ refine Numeric do
9
+ def mm
10
+ self * 72 / 25.4
11
+ end
12
+ end
13
+ end
14
+
15
+ using HexaPDF::Extras::Layout::NumericMeasurementHelper
16
+
17
+ module HexaPDF
18
+ module Extras
19
+ module Layout
20
+
21
+ # Displays a Swiss QR-bill.
22
+ #
23
+ # This class implements version 2.2 of the Swiss QR-bill specification but takes into account
24
+ # version 2.3 where appropriate.
25
+ #
26
+ #
27
+ # == Requirements
28
+ #
29
+ # * Liberation Sans TrueType font installed in standard Linux path (or changed via the
30
+ # configuration option, see next section)
31
+ # * Rubygem +rqrcode_core+ for generating the QR code
32
+ #
33
+ #
34
+ # == Configuration option 'layout.swiss_qr_bill'
35
+ #
36
+ # The configuration option 'layout.swiss_qr_bill' is a hash containing styling information for
37
+ # the various text parts: section heading, payment heading/value, receipt heading/value and
38
+ # the alternative procedures. The default values are taking from the QR-bill style guide.
39
+ #
40
+ # The keys of this hash are strings of the form 'part.subpart.property' where
41
+ #
42
+ # * 'part' can be 'section', 'payment', 'receipt', or 'alternative_procedures',
43
+ # * 'subpart' can be 'heading' or 'value' (the latter not for 'section'),
44
+ # * 'property' can be 'font', 'font_size' or 'line_height' (the latter not for 'section').
45
+ #
46
+ # The default font is Liberation Sans which is one of the four allowed fonts (the others being
47
+ # Arial, Frutiger, and Helvetica). The font files themselves are *not* included and the 'font'
48
+ # property, by default, references the standard Linux path where the fonts would be found.
49
+ # Note that all '*.heading.font' values should reference the bold version of the font whereas
50
+ # the '*.value.font' values should reference the regular version.
51
+ #
52
+ #
53
+ # == Data Structure
54
+ #
55
+ # All the necessary information for generating the Swiss QR-bill is provided on
56
+ # initialization. The following keys can be used:
57
+ #
58
+ # :lang::
59
+ # The language to use for the literal text strings appearing in the QR-bill. One of :en,
60
+ # :de, :fr or :it.
61
+ #
62
+ # Defaults to :en if not specified.
63
+ #
64
+ # :creditor::
65
+ # (required) The creditor of the transaction. This is a hash that can contain the
66
+ # following elements:
67
+ #
68
+ # :iban::
69
+ # (required) The IBAN of the creditor (21 characters, no spaces, only IBANs for CH or
70
+ # LI). Note that the IBAN is not checked for validity.
71
+ #
72
+ # :name::
73
+ # (required) The name of the creditor (maximum 70 characters).
74
+ #
75
+ # :address_type::
76
+ # (required) The type of address, either :structured or :combined. Defaults to
77
+ # :structured which is the only choice in version 2.3 of the specification.
78
+ #
79
+ # :address_line1::
80
+ # The first line of the creditor's address (maximum 70 characters). In case of a
81
+ # structured address, this is the street. Otherwise this has to be the street and
82
+ # building number together.
83
+ #
84
+ # :address_line2::
85
+ # The second line of the creditor's address. In case of a structured address, this
86
+ # has to be the building number (maximum 16 characters). Otherwise it has to be the
87
+ # postal code and town (maximum 70 characters).
88
+ #
89
+ # :postal_code::
90
+ # The postal code of the creditor's address (maximum 16 characters, only for
91
+ # structured addresses).
92
+ #
93
+ # :town::
94
+ # The town from the creditor's address (maximum 35 characters, only for structured
95
+ # addresses).
96
+ #
97
+ # :country::
98
+ # (required) The country from the creditor's address (ISO 3166-1 two-letter country
99
+ # code).
100
+ #
101
+ # :debtor::
102
+ # The debtor information for the transaction. This information is optional but if used
103
+ # some elements are required. The value is a hash that can contain the same elements as
104
+ # the +:creditor+ key with the exception of the +:iban+.
105
+ #
106
+ # :amount::
107
+ # The payment amount (between 0.01 and 999,999,999.99). If not filled in, a blank field is
108
+ # shown for adding the amount by hand later. If the amount is set to zero, it means that
109
+ # the QR-bill should be used as notification and the :message is set according to the
110
+ # specification.
111
+ #
112
+ # :currency::
113
+ # (required) The payment currency (either CHF or EUR).
114
+ #
115
+ # :reference_type::
116
+ # The payment reference type (either QRR, SCOR or NON). Defaults to NON.
117
+ #
118
+ # :reference::
119
+ # The structured reference data. All whitespace is removed before processing.
120
+ #
121
+ # In case of a QRR reference, the value has to be a 26 digit reference without check digit
122
+ # or a 27 digit reference with check digit. The check digit is validated.
123
+ #
124
+ # In case of a SCOR reference, the value has to contain between 5 and 25 alpha-numeric
125
+ # characters. The check digits are validated.
126
+ #
127
+ # :message::
128
+ # Additional, unstructured information (maximum 140 characters).
129
+ #
130
+ # :billing_information::
131
+ # Billing information for automated booking of the payment (maximum 140 characters).
132
+ #
133
+ # :alternative_schemes::
134
+ # Alternative schemes parameters. Either a single string with a maximum of 100 characters
135
+ # or an array of two such strings.
136
+ #
137
+ # == Example
138
+ #
139
+ # HexaPDF::Composer.create("sample-qr-bill.pdf", margin: 0) do |composer|
140
+ # data = {
141
+ # lang: :de,
142
+ # creditor: {
143
+ # iban: "CH44 3199 9123 0008 8901 2",
144
+ # name: "Max Muster & Söhne",
145
+ # address_line1: "Musterstrasse",
146
+ # address_line2: "123",
147
+ # postal_code: "8000",
148
+ # town: "Seldwyla",
149
+ # country: "CH",
150
+ # },
151
+ # debtor: {
152
+ # address_type: :combined,
153
+ # name: "Simon Muster",
154
+ # address_line1: "Musterstrasse 1",
155
+ # address_line2: "8000 Seldwyla",
156
+ # country: "CH"
157
+ # },
158
+ # amount: 2500.25,
159
+ # currency: 'CHF',
160
+ # }
161
+ # composer.swiss_qr_bill(data: data, valign: :bottom)
162
+ # end
163
+ #
164
+ # == References
165
+ #
166
+ # * Website https://www.six-group.com/en/products-services/banking-services/billing-and-payments/qr-bill.html
167
+ # * 2.2 Specification https://www.six-group.com/dam/download/banking-services/standardization/qr-bill/ig-qr-bill-v2.2-en.pdf
168
+ # * 2.3 Specification https://www.six-group.com/dam/download/banking-services/standardization/qr-bill/ig-qr-bill-v2.3-en.pdf
169
+ # * Style guide https://www.six-group.com/dam/download/banking-services/standardization/qr-bill/style-guide-qr-bill-en.pdf
170
+ class SwissQRBill < HexaPDF::Layout::Box
171
+
172
+ # Thrown when an error occurs when working with the SwissQRBill class.
173
+ class Error < HexaPDF::Error
174
+ end
175
+
176
+ # Mapping of the English text literals to their German, French and Italian counterparts,
177
+ # taken from Annex C of the specification.
178
+ TEXT_LITERALS = { #:nodoc:
179
+ de: {
180
+ 'Payment part' => 'Zahlteil',
181
+ 'Receipt' => 'Empfangsschein',
182
+ 'Account / Payable to' => 'Konto / Zahlbar an',
183
+ 'Reference' => 'Referenz',
184
+ 'Additional information' => 'Zusätzliche Informationen',
185
+ 'Payable by' => 'Zahlbar durch',
186
+ 'Payable by (name/address)' => 'Zahlbar durch (Name/Adresse)',
187
+ 'Currency' => 'Währung',
188
+ 'Amount' => 'Betrag',
189
+ 'Acceptance point' => 'Annahmestelle',
190
+ 'In favour of' => 'Zugunsten',
191
+ 'DO NOT USE FOR PAYMENT' => 'NICHT ZUR ZAHLUNG VERWENDEN',
192
+ },
193
+ fr: {
194
+ 'Payment part' => 'Section paiement',
195
+ 'Receipt' => 'Récépissé',
196
+ 'Account / Payable to' => 'Compte / Payable à',
197
+ 'Reference' => 'Référence',
198
+ 'Additional information' => 'Information supplémentaires',
199
+ 'Payable by' => 'Payable par',
200
+ 'Payable by (name/address)' => 'Payable par (nom/address)',
201
+ 'Currency' => 'Monnaie',
202
+ 'Amount' => 'Montant',
203
+ 'Acceptance point' => 'Point de dépôt',
204
+ 'In favour of' => 'En faveur de',
205
+ 'DO NOT USE FOR PAYMENT' => 'NE PAS UTILISER POUR LE PAIEMENT',
206
+ },
207
+ it: {
208
+ 'Payment part' => 'Sezione pagamento',
209
+ 'Receipt' => 'Ricevuta',
210
+ 'Account / Payable to' => 'Conto / Pagabile a',
211
+ 'Reference' => 'Riferimento',
212
+ 'Additional information' => 'Informazioni supplementari',
213
+ 'Payable by' => 'Pagabile da',
214
+ 'Payable by (name/address)' => 'Pagabile da (nome/indirizzo)',
215
+ 'Currency' => 'Value date ',
216
+ 'Amount' => 'Importo',
217
+ 'Acceptance point' => 'Punto di accettazione',
218
+ 'In favour of' => 'A favore di',
219
+ 'DO NOT USE FOR PAYMENT' => 'NON UTILIZZARE PER IL PAGAMENTO',
220
+ }
221
+ }
222
+
223
+ # The payment data - see the SwissQRBill class documentation for details.
224
+ attr_reader :data
225
+
226
+ # Creates a new SwissQRBill object for the given payment +data+ (see the class documentation
227
+ # for details).
228
+ #
229
+ # If the arguments +width+ and +height+ are provided, they are ignored since the QR-bill has
230
+ # a fixed size of 210mm x 105mm.
231
+ def initialize(data:, **kwargs)
232
+ super(**kwargs, width: 210.mm, height: 105.mm)
233
+ validate_data(data)
234
+ end
235
+
236
+ private
237
+
238
+ QRR_MODULO10_TABLE = [ #:nodoc:
239
+ [0, 9, 4, 6, 8, 2, 7, 1, 3, 5],
240
+ [9, 4, 6, 8, 2, 7, 1, 3, 5, 0],
241
+ [4, 6, 8, 2, 7, 1, 3, 5, 0, 9],
242
+ [6, 8, 2, 7, 1, 3, 5, 0, 9, 4],
243
+ [8, 2, 7, 1, 3, 5, 0, 9, 4, 6],
244
+ [2, 7, 1, 3, 5, 0, 9, 4, 6, 8],
245
+ [7, 1, 3, 5, 0, 9, 4, 6, 8, 2],
246
+ [1, 3, 5, 0, 9, 4, 6, 8, 2, 7],
247
+ [3, 5, 0, 9, 4, 6, 8, 2, 7, 1],
248
+ [5, 0, 9, 4, 6, 8, 2, 7, 1, 3]
249
+ ].freeze
250
+
251
+ # Validates the given data and raises an error if the data is not valid
252
+ def validate_data(data)
253
+ @data = data
254
+
255
+ if @data[:currency] != 'EUR' && @data[:currency] != 'CHF'
256
+ raise Error, "Data field :currency must be EUR or CHR, not #{@data[:currency].inspect}"
257
+ end
258
+
259
+ if @data[:amount] == 0
260
+ @data[:message] = text('DO NOT USE FOR PAYMENT')
261
+ elsif @data[:amount] && (@data[:amount] < 0.01 || @data[:amount] > 999_999_999.99)
262
+ raise Error, "Data field :amount must be between 0.01 and 999_999_999.99"
263
+ end
264
+
265
+ validate_address = lambda do |hash|
266
+ hash[:address_type] ||= :structured
267
+ if hash[:address_type] != :structured && hash[:address_type] != :combined
268
+ raise Error, "Address type must be :structured or :combined, not #{hash[:address_type]}"
269
+ end
270
+ structured = (hash[:address_type] == :structured)
271
+
272
+ if (value = hash[:name]) && value.size > 70
273
+ raise Error, "Name in addresss must not contain more than 70 characters"
274
+ elsif !value
275
+ raise Error, "Name in address must be provided"
276
+ end
277
+
278
+ if hash[:address_line1] && hash[:address_line1].size > 70
279
+ raise Error, "Address line 1 must not contain more than 70 characters"
280
+ end
281
+
282
+ if (value = hash[:address_line2])
283
+ if structured && value.size > 16
284
+ raise Error, "Address line 2 of a structured address must not contain " \
285
+ "more than 16 characters"
286
+ elsif value.size > 70
287
+ raise Error, "Address line 2 of a combined address must not contain " \
288
+ "more than 70 characters"
289
+ end
290
+ elsif !structured
291
+ raise Error, "Address line 2 must be provided for a combined address"
292
+ end
293
+
294
+ if (value = hash[:postal_code])
295
+ if !structured
296
+ raise Error, "Postal code must not be provided for a combined address"
297
+ elsif value.size > 16
298
+ raise Error, "Postal code must not contain more than 16 characters"
299
+ end
300
+ elsif structured
301
+ raise Error, "Postal code must be provided for a structured address"
302
+ end
303
+
304
+ if (value = hash[:town])
305
+ if !structured
306
+ raise Error, "Town must not be provided for a combined address"
307
+ elsif value.size > 35
308
+ raise Error, "Town must not contain more than 35 characters"
309
+ end
310
+ elsif structured
311
+ raise Error, "Town must be provided for a structured address"
312
+ end
313
+
314
+ if (value = hash[:country]) && value.size != 2
315
+ raise Error, "Country must be a two-letter ISO-3166-1 code"
316
+ elsif !value
317
+ raise Error, "Country must be provided"
318
+ end
319
+ end
320
+ validate_address.call(@data[:creditor])
321
+ validate_address.call(@data[:debtor]) if @data[:debtor]
322
+
323
+ @data[:reference_type] ||= "NON"
324
+ case @data[:reference_type]
325
+ when "QRR"
326
+ value = @data[:reference]
327
+ unless value
328
+ raise Error, "Data field :reference must be filled in for QRR reference type"
329
+ end
330
+ value.gsub!(/\s*/, '')
331
+ if value !~ /\A\d{26,27}\z/
332
+ raise Error, "Data field :reference for QRR must contain 26 or 27 digits"
333
+ end
334
+ result = value[0, 26].each_codepoint.inject(0) do |memo, codepoint|
335
+ QRR_MODULO10_TABLE[memo][codepoint - 48]
336
+ end
337
+ check_digit = (10 - result) % 10
338
+ value << check_digit.to_s if value.size == 26
339
+ if value[26].to_i != check_digit
340
+ raise Error, "Data field :reference for QRR contains an invalid check digit, " \
341
+ "should be #{check_digit}"
342
+ end
343
+ when "SCOR"
344
+ value = @data[:reference]
345
+ unless value
346
+ raise Error, "Data field :reference must be filled in for SCOR reference type"
347
+ end
348
+ value.gsub!(/\s*/, '')
349
+ if value !~ /\A\w{5,25}\z/
350
+ raise Error, "Data field :reference for SCOR must contain between 5 and 25 " \
351
+ "alpha-numeric characters"
352
+ elsif value !~ /\ARF\d\d/
353
+ raise Error, "Data field :reference for SCOR must start with RF and check digits"
354
+ end
355
+ # See https://www.mobilefish.com/services/creditor_reference/creditor_reference.php
356
+ result = "#{value[4..-1]}#{value[0, 4]}".upcase.gsub(/[A-Z]/) {|c| c.ord - 55 }.to_i % 97
357
+ unless result == 1
358
+ raise Error, "Data field :reference for SCOR has invalid check digits"
359
+ end
360
+ when "NON"
361
+ if @data[:reference]
362
+ raise Error, "Data field :reference must not be provided for NON reference type"
363
+ end
364
+ else
365
+ raise Error, "Data field :reference_type must be one of QRR, SCOR or NON"
366
+ end
367
+
368
+ if @data[:message] && @data[:message].size > 140
369
+ raise Error, "Data field :message must not contain more than 140 characters"
370
+ end
371
+ if @data[:billing_information] && @data[:billing_information].size > 140
372
+ raise Error, "Data field :billing_information must not contain more than 140 characters"
373
+ end
374
+ if (@data[:message].to_s + @data[:billing_information].to_s).size > 140
375
+ raise Error, "Data fields :message and :billing_information together must not " \
376
+ "contain more than 140 characters"
377
+ end
378
+
379
+ @data[:alternative_schemes] = Array(@data[:alternative_schemes])
380
+ if @data[:alternative_schemes].any? {|as| as.size > 100 } ||
381
+ @data[:alternative_schemes].size > 2
382
+ raise Error, "Data field :alternative_schemes must at most contain 2 strings with " \
383
+ "not more than 100 characters each"
384
+ end
385
+ end
386
+
387
+ # Draws the SwissQRBill onto the canvas at position [x, y].
388
+ def draw_content(canvas, x, y)
389
+ layout = canvas.context.document.layout
390
+ frame = HexaPDF::Layout::Frame.new(0, 0, width, height, context: canvas.context)
391
+ box_fitter = HexaPDF::Layout::BoxFitter.new([frame])
392
+ styles = set_up_styles(canvas.context.document.config['layout.swiss_qr_bill'], layout)
393
+
394
+ box_fitter.fit(receipt(layout, styles))
395
+ box_fitter.fit(payment(layout, styles, qr_code_cross(canvas.context.document)))
396
+ unless box_fitter.fit_successful?
397
+ raise HexaPDF::Error, "The Swiss QR-bill could not be fit"
398
+ end
399
+
400
+ canvas.translate(x, y) do
401
+ box_fitter.fit_results.each {|fit_result| fit_result.draw(canvas)}
402
+ canvas.stroke_color(0).line_width(0.5).line_dash_pattern(2).
403
+ line(62.mm, 0, 62.mm, 105.mm).
404
+ line(0, 105.mm, 210.mm, 105.mm).stroke
405
+ canvas.font('ZapfDingbats', size: 15).text("✂", at: [5.mm, 103.1.mm])
406
+ canvas.font('ZapfDingbats', size: 15).text_matrix(0, -1, 1, 0, 60.175.mm, 100.mm).text("✂")
407
+ end
408
+ end
409
+
410
+ # Returns a hash with styles that are used throughout the QR-bill creation for consistency.
411
+ def set_up_styles(config, layout)
412
+ {
413
+ section_heading: {font_size: config['section.heading.font_size'],
414
+ font: config['section.heading.font']},
415
+ payment_heading: {font_size: config['payment.heading.font_size'],
416
+ font: config['payment.heading.font'],
417
+ line_height: config['payment.heading.line_height'],
418
+ line_spacing: {type: :fixed, value: config['payment.heading.line_height']}},
419
+ payment_value: {font_size: config['payment.value.font_size'],
420
+ font: config['payment.value.font'],
421
+ line_height: config['payment.value.line_height'],
422
+ line_spacing: {type: :fixed, value: config['payment.value.line_height']},
423
+ padding: [0, 0, config['payment.value.line_height']]},
424
+ receipt_heading: {font_size: config['receipt.heading.font_size'],
425
+ font: config['receipt.heading.font'],
426
+ line_height: config['receipt.heading.line_height'],
427
+ line_spacing: {type: :fixed, value: config['receipt.heading.line_height']}},
428
+ receipt_value: {font_size: config['receipt.value.font_size'],
429
+ font: config['receipt.value.font'],
430
+ line_height: config['receipt.value.line_height'],
431
+ line_spacing: {type: :fixed, value: config['receipt.value.line_height']},
432
+ padding: [0, 0, config['receipt.value.line_height']]},
433
+ alternative_procedures_heading: {
434
+ font_size: config['alternative_procedures.heading.font_size'],
435
+ font: config['alternative_procedures.heading.font'],
436
+ line_height: config['alternative_procedures.heading.line_height'],
437
+ line_spacing: {type: :fixed, value: config['alternative_procedures.heading.line_height']}
438
+ },
439
+ alternative_procedures_value: {
440
+ font_size: config['alternative_procedures.value.font_size'],
441
+ font: config['alternative_procedures.value.font'],
442
+ line_height: config['alternative_procedures.value.line_height'],
443
+ line_spacing: {type: :fixed, value: config['alternative_procedures.value.line_height']},
444
+ padding: [0, 0, config['alternative_procedures.value.line_height']]
445
+ },
446
+ }.transform_values! {|value| layout.style(:base).dup.update(**value) }
447
+ end
448
+
449
+ # Returns a box containing the receipt part of the QR-bill.
450
+ def receipt(layout, styles)
451
+ layout.container(width: 62.mm, style: {padding: 5.mm, mask_mode: :fill_vertical}) do |col|
452
+ col.text(text('Receipt'), height: 7.mm, style: styles[:section_heading])
453
+ col.container(height: 56.mm) do |info|
454
+ info.text(text('Account / Payable to'), style: styles[:receipt_heading])
455
+ info.text("#{@data[:creditor][:iban]}\n#{address(@data[:creditor])}", style: styles[:receipt_value])
456
+
457
+ if @data[:reference_type] != 'NON'
458
+ info.text(text('Reference'), style: styles[:receipt_heading])
459
+ info.text(@data[:reference], style: styles[:receipt_value])
460
+ end
461
+
462
+ if @data[:debtor]
463
+ info.text(text('Payable by'), style: styles[:receipt_heading])
464
+ info.text(address(@data[:debtor]), style: styles[:receipt_value])
465
+ else
466
+ info.text(text('Payable by (name/address)'), style: styles[:receipt_heading])
467
+ blank_field(info, 52.mm, 20.mm)
468
+ end
469
+ end
470
+ receipt_amount(col, styles)
471
+ col.text(text('Acceptance point'), style: styles[:receipt_heading], text_align: :right)
472
+ end
473
+ end
474
+
475
+ # Adds the appropriate boxes to the given composer for the amount part of the receipt
476
+ # section.
477
+ def receipt_amount(composer, styles)
478
+ composer.container(height: 14.mm) do |amount|
479
+ if @data[:amount]
480
+ amount.column(columns: [26.mm, -1], gaps: 0) do |inner|
481
+ inner.text(text('Currency'), style: styles[:receipt_heading], padding: [0, 0, 1.mm])
482
+ inner.text(@data[:currency], style: styles[:receipt_value])
483
+ inner.text(text('Amount'), style: styles[:receipt_heading], padding: [0, 0, 1.mm])
484
+ inner.text(formatted_amount, style: styles[:receipt_value])
485
+ end
486
+ else
487
+ amount.column(columns: [-1, 31.mm], gaps: 0) do |inner|
488
+ inner.text(text('Currency') + " " + text("Amount"),
489
+ style: styles[:receipt_heading], padding: [0, 0, 1.mm])
490
+ inner.text(@data[:currency], style: styles[:receipt_value], mask_mode: :fill)
491
+ inner.box(:base, height: 2)
492
+ blank_field(inner, 30.mm, 10.mm)
493
+ end
494
+ end
495
+ end
496
+ end
497
+
498
+ # Returns a box containing the payment part of the QR-bill.
499
+ def payment(layout, styles, cross)
500
+ layout.container(width: 148.mm, style: {padding: 5.mm}) do |col|
501
+ col.container(width: 51.mm, height: 85.mm, style: {mask_mode: :box}) do |left_col|
502
+ left_col.text(text('Payment part'), height: 7.mm, style: styles[:section_heading])
503
+ left_col.box(:qrcode, data: qr_code_data, level: :m, style: {padding: [5.mm, 5.mm, 5.mm, 0.mm]})
504
+ left_col.image(cross, width: 7.mm, position: [19.5.mm, 46.5.mm])
505
+ payment_amount(left_col, styles)
506
+ end
507
+ col.container(height: 85.mm) do |info|
508
+ info.text(text('Account / Payable to'), style: styles[:payment_heading])
509
+ info.text("#{@data[:creditor][:iban]}\n#{address(@data[:creditor])}", style: styles[:payment_value])
510
+
511
+ if @data[:reference_type] != 'NON'
512
+ info.text(text('Reference'), style: styles[:payment_heading])
513
+ info.text(@data[:reference], style: styles[:payment_value])
514
+ end
515
+
516
+ if @data[:message] || @data[:billing_information]
517
+ info.text(text('Additional information'), style: styles[:payment_heading])
518
+ info.text([@data[:message], @data[:billing_information]].compact.join("\n"), style: styles[:payment_value])
519
+ end
520
+
521
+ if @data[:debtor]
522
+ info.text(text('Payable by'), style: styles[:payment_heading])
523
+ info.text(address(@data[:debtor]), style: styles[:payment_value])
524
+ else
525
+ info.text(text('Payable by (name/address)'), style: styles[:payment_heading])
526
+ blank_field(info, 65.mm, 25.mm)
527
+ end
528
+ end
529
+ if @data[:alternative_schemes].size > 0
530
+ @data[:alternative_schemes].each do |as|
531
+ provider, data = *as.split('/', 2)
532
+ col.formatted_text([{text: provider, style: styles[:alternative_procedures_heading]},
533
+ {text: data, style: styles[:alternative_procedures_value]}])
534
+ end
535
+ end
536
+ end
537
+ end
538
+
539
+ # Adds the appropriate boxes to the given composer for the amount part of the payment
540
+ # section.
541
+ def payment_amount(composer, styles)
542
+ composer.container(height: 22.mm) do |amount|
543
+ if @data[:amount]
544
+ amount.column(columns: [23.mm, -1], gaps: 0) do |inner|
545
+ inner.text(text('Currency'), style: styles[:payment_heading], padding: [0, 0, 1.mm])
546
+ inner.text(@data[:currency], style: styles[:payment_value])
547
+ inner.text(text('Amount'), style: styles[:payment_heading], padding: [0, 0, 1.mm])
548
+ inner.text(formatted_amount, style: styles[:payment_value])
549
+ end
550
+ else
551
+ amount.text(text('Currency') + " " + text("Amount"),
552
+ style: styles[:payment_heading], padding: [0, 0, 1.mm])
553
+ amount.column(columns: [-1, 41.mm], gaps: 0) do |inner|
554
+ inner.text(@data[:currency], style: styles[:payment_value])
555
+ blank_field(inner, 40.mm, 15.mm)
556
+ end
557
+ end
558
+ end
559
+ end
560
+
561
+ # Returns the correctly localized text for the given string +str+.
562
+ def text(str)
563
+ TEXT_LITERALS.dig(@data[:lang], str) || str
564
+ end
565
+
566
+ # Returns a string containing the formatted address for output using the provided data.
567
+ def address(data)
568
+ result = +''
569
+ result << "#{data[:name]}\n"
570
+ if data[:address_type] == :structured
571
+ addr = [data[:address_line1], data[:address_line2]].compact
572
+ result << "#{addr.join(' ')}\n" unless addr.empty?
573
+ result << "#{data[:country]}-#{data[:postal_code]} #{data[:town]}"
574
+ else
575
+ result << "#{data[:address_line1]}\n" if data.key?(:address_line1)
576
+ result << "#{data[:country]}-#{data[:address_line2]}\n"
577
+ end
578
+ result
579
+ end
580
+
581
+ # Returns the amount formatted according to the specification.
582
+ def formatted_amount
583
+ a, b = format('%.2f', @data[:amount]).split('.')
584
+ a.gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1 ") << '.' << b
585
+ end
586
+
587
+ # Creates the content of the QR code using the information provided in #data.
588
+ def qr_code_data
589
+ qr_code_data = []
590
+ add_address_data = lambda do |hash|
591
+ qr_code_data << (hash[:address_type] == :structured ? "S" : "K")
592
+ qr_code_data << hash[:name] <<
593
+ hash[:address_line1].to_s << hash[:address_line2].to_s <<
594
+ hash[:postal_code].to_s << hash[:town].to_s << hash[:country]
595
+ end
596
+
597
+ # Header information
598
+ qr_code_data.concat(["SPC", "0200", "1"])
599
+
600
+ # Creditor information
601
+ qr_code_data << @data[:creditor][:iban]
602
+ add_address_data.call(@data[:creditor])
603
+
604
+ # Ultimate creditor information
605
+ qr_code_data.concat(["", "", "", "", "", "", ""])
606
+
607
+ # Amount information
608
+ qr_code_data << (@data[:amount] ? format('%.2f', @data[:amount]) : "") << @data[:currency]
609
+
610
+ # Debtor information
611
+ add_address_data.call(@data[:debtor]) if @data[:debtor]
612
+
613
+ # Payment reference
614
+ qr_code_data << @data[:reference_type] << @data[:reference].to_s <<
615
+ @data[:message].to_s << "EPD"
616
+ qr_code_data << @data[:billing_information] if @data[:billing_information]
617
+
618
+ qr_code_data.join("\r\n")
619
+ end
620
+
621
+ # Creates a Form XObject for the Swiss cross that is to be overlaid over the QR code.
622
+ #
623
+ # The measurements are reverse-engineered from the images provided at
624
+ # https://www.six-group.com/dam/download/banking-services/standardization/qr-bill/swiss-cross-graphic-en.zip
625
+ def qr_code_cross(document)
626
+ cross = document.add({Type: :XObject, Subtype: :Form, BBox: [0, 0, 7.mm, 7.mm]})
627
+ canvas = cross.canvas
628
+ canvas.fill_color(1.0).rectangle(0, 0, 7.mm, 7.mm).fill
629
+ canvas.fill_color(0.0).rectangle(0.5.mm, 0.5.mm, 6.mm, 6.mm).fill
630
+ canvas.fill_color(1.0).
631
+ rectangle(2.93.mm, 1.67.mm, 1.17.mm, 3.9.mm).
632
+ rectangle(1.57.mm, 3.04.mm, 3.9.mm, 1.17.mm).
633
+ fill
634
+ cross
635
+ end
636
+
637
+ CORNER_MARK_LENGTH = 3.mm # :nodoc:
638
+
639
+ # Creates a blank rectangle of the given +width+ and +height+, with corner marks as
640
+ # specified by the specification.
641
+ def blank_field(layout, width, height)
642
+ layout.box(width: width, height: height) do |canvas, box|
643
+ canvas.stroke_color(0).line_width(0.75).
644
+ polyline(0, CORNER_MARK_LENGTH, 0, 0, CORNER_MARK_LENGTH, 0).
645
+ polyline(width - CORNER_MARK_LENGTH, 0, width, 0, width, CORNER_MARK_LENGTH).
646
+ polyline(width, height - CORNER_MARK_LENGTH, width, height, width - CORNER_MARK_LENGTH, height).
647
+ polyline(CORNER_MARK_LENGTH, height, 0, height, 0, height - CORNER_MARK_LENGTH).
648
+ stroke
649
+ end
650
+ end
651
+
652
+ end
653
+
654
+ end
655
+ end
656
+ end
@@ -4,6 +4,7 @@ module HexaPDF
4
4
  module Extras
5
5
  module Layout
6
6
  autoload(:QRCodeBox, 'hexapdf/extras/layout/qr_code_box')
7
+ autoload(:SwissQRBill, 'hexapdf/extras/layout/swiss_qr_bill')
7
8
  end
8
9
  end
9
10
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module HexaPDF
4
4
  module Extras
5
- VERSION = '1.0.1'
5
+ VERSION = '1.1.1'
6
6
  end
7
7
  end
@@ -14,3 +14,33 @@ HexaPDF::DefaultDocumentConfiguration['graphic_object.map'][:qrcode] =
14
14
 
15
15
  HexaPDF::DefaultDocumentConfiguration['layout.boxes.map'][:qrcode] =
16
16
  'HexaPDF::Extras::Layout::QRCodeBox'
17
+
18
+ HexaPDF::DefaultDocumentConfiguration['layout.boxes.map'][:swiss_qr_bill] =
19
+ 'HexaPDF::Extras::Layout::SwissQRBill'
20
+
21
+ # These values comes from the "Style Guide QR-bill", available at
22
+ # https://www.six-group.com/dam/download/banking-services/standardization/qr-bill/style-guide-qr-bill-en.pdf
23
+ font = '/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf'
24
+ font_bold = '/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf'
25
+ HexaPDF::DefaultDocumentConfiguration['layout.swiss_qr_bill'] = {
26
+ 'section.heading.font' => font_bold,
27
+ 'section.heading.font_size' => 11,
28
+ 'payment.heading.font' => font_bold,
29
+ 'payment.heading.font_size' => 8,
30
+ 'payment.heading.line_height' => 11,
31
+ 'payment.value.font' => font,
32
+ 'payment.value.font_size' => 10,
33
+ 'payment.value.line_height' => 11,
34
+ 'receipt.heading.font' => font_bold,
35
+ 'receipt.heading.font_size' => 6,
36
+ 'receipt.heading.line_height' => 9,
37
+ 'receipt.value.font' => font,
38
+ 'receipt.value.font_size' => 8,
39
+ 'receipt.value.line_height' => 9,
40
+ 'alternative_procedures.heading.font' => font_bold,
41
+ 'alternative_procedures.heading.font_size' => 7,
42
+ 'alternative_procedures.heading.line_height' => 8,
43
+ 'alternative_procedures.value.font' => font,
44
+ 'alternative_procedures.value.font_size' => 7,
45
+ 'alternative_procedures.value.line_height' => 8,
46
+ }
@@ -7,6 +7,10 @@ describe HexaPDF::Extras::Layout::QRCodeBox do
7
7
  HexaPDF::Extras::Layout::QRCodeBox.new(**kwargs)
8
8
  end
9
9
 
10
+ before do
11
+ @frame = HexaPDF::Layout::Frame.new(0, 0, 100, 100)
12
+ end
13
+
10
14
  describe "initialize" do
11
15
  it "can take the common box arguments" do
12
16
  box = create_box(width: 10, height: 15)
@@ -25,7 +29,7 @@ describe HexaPDF::Extras::Layout::QRCodeBox do
25
29
  it "uses the smaller value of width/height for the dimensions if smaller than available_width/height" do
26
30
  [{width: 10}, {width: 10, height: 50}, {height: 10}, {width: 50, height: 10}].each do |args|
27
31
  box = create_box(**args)
28
- assert(box.fit(100, 100, nil))
32
+ assert(box.fit(100, 100, @frame))
29
33
  assert_equal(10, box.width)
30
34
  assert_equal(10, box.height)
31
35
  assert_equal(10, box.qr_code.size)
@@ -34,12 +38,12 @@ describe HexaPDF::Extras::Layout::QRCodeBox do
34
38
 
35
39
  it "uses the smaller value of available_width/height for the dimensions" do
36
40
  box = create_box
37
- assert(box.fit(10, 20, nil))
41
+ assert(box.fit(10, 20, @frame))
38
42
  assert_equal(10, box.width)
39
43
  assert_equal(10, box.height)
40
44
  assert_equal(10, box.qr_code.size)
41
45
 
42
- assert(box.fit(20, 15, nil))
46
+ assert(box.fit(20, 15, @frame))
43
47
  assert_equal(15, box.width)
44
48
  assert_equal(15, box.height)
45
49
  assert_equal(15, box.qr_code.size)
@@ -47,19 +51,19 @@ describe HexaPDF::Extras::Layout::QRCodeBox do
47
51
 
48
52
  it "takes the border and padding into account for the QR code size" do
49
53
  box = create_box(style: {padding: [1, 2], border: {width: [3, 4]}})
50
- assert(box.fit(100, 100, nil))
54
+ assert(box.fit(100, 100, @frame))
51
55
  assert_equal(88, box.qr_code.size)
52
56
  assert_equal(100, box.width)
53
57
  assert_equal(96, box.height)
54
58
 
55
59
  box = create_box(style: {padding: [2, 1], border: {width: [4, 3]}})
56
- assert(box.fit(100, 100, nil))
60
+ assert(box.fit(100, 100, @frame))
57
61
  assert_equal(88, box.qr_code.size)
58
62
  assert_equal(96, box.width)
59
63
  assert_equal(100, box.height)
60
64
 
61
65
  box = create_box(style: {padding: [5, 5, 5, 0], border: {width: [2, 2, 2, 0]}})
62
- assert(box.fit(50, 100, nil))
66
+ assert(box.fit(50, 100, @frame))
63
67
  assert_equal(43, box.qr_code.size)
64
68
  assert_equal(50, box.width)
65
69
  assert_equal(57, box.height)
@@ -69,7 +73,7 @@ describe HexaPDF::Extras::Layout::QRCodeBox do
69
73
  describe "draw" do
70
74
  it "draws the qrcode" do
71
75
  box = create_box(width: 10)
72
- box.fit(100, 100, nil)
76
+ box.fit(100, 100, @frame)
73
77
 
74
78
  canvas = Minitest::Mock.new
75
79
  canvas.expect(:draw, nil, [box.qr_code])
@@ -0,0 +1,233 @@
1
+ require 'test_helper'
2
+ require 'hexapdf'
3
+ require 'hexapdf/extras'
4
+ require 'hexapdf/extras/layout/swiss_qr_bill'
5
+
6
+ using HexaPDF::Extras::Layout::NumericMeasurementHelper
7
+
8
+ describe HexaPDF::Extras::Layout::SwissQRBill do
9
+ def create_box(data, **kwargs)
10
+ HexaPDF::Extras::Layout::SwissQRBill.new(data: data, **kwargs)
11
+ end
12
+
13
+ def data
14
+ @data ||= {
15
+ creditor: {
16
+ iban: "CH44 3199 9123 0008 8901 2",
17
+ name: "Max Muster & Söhne",
18
+ address_line1: "Musterstrasse",
19
+ address_line2: "123",
20
+ postal_code: "8000",
21
+ town: "Seldwyla",
22
+ country: "CH",
23
+ },
24
+ debtor: {
25
+ address_type: :combined,
26
+ name: "Simon Muster",
27
+ address_line1: "Musterstrasse 1",
28
+ address_line2: "8000 Seldwyla",
29
+ country: "CH"
30
+ },
31
+ lang: :de,
32
+ amount: 2500.25,
33
+ currency: 'CHF',
34
+ }
35
+ end
36
+
37
+ before do
38
+ @error_class = HexaPDF::Extras::Layout::SwissQRBill::Error
39
+ end
40
+
41
+ describe "initialize" do
42
+ it "overrides any set width or height" do
43
+ box = create_box(data, width: 10, height: 15)
44
+ assert_equal(210.mm, box.width)
45
+ assert_equal(105.mm, box.height)
46
+ end
47
+
48
+ describe "data validation" do
49
+ def assert_invalid_data(data = self.data, message)
50
+ err = assert_raises(@error_class) { create_box(data) }
51
+ assert_match(message, err.message)
52
+ end
53
+
54
+ it "ensures a correct currency value" do
55
+ assert_equal('EUR', create_box(data.update(currency: 'EUR')).data[:currency])
56
+ assert_equal('CHF', create_box(data.update(currency: 'CHF')).data[:currency])
57
+ assert_invalid_data(data.update(currency: 'USD'), /field :currency/)
58
+ end
59
+
60
+ it "ensure a correct amount value" do
61
+ assert_invalid_data(data.update(amount: 0.001), /field :amount/)
62
+ assert_invalid_data(data.update(amount: 1_000_000_000), /field :amount/)
63
+ data.update(amount: 0.00)
64
+ assert_equal('NICHT ZUR ZAHLUNG VERWENDEN', create_box(data).data[:message])
65
+ end
66
+
67
+ it "sets the address type to structured by default" do
68
+ assert_equal(:structured, create_box(data, width: 10, height: 15).data[:creditor][:address_type])
69
+ end
70
+
71
+ it "ensures a correct address type" do
72
+ data[:creditor].update(address_type: :invalid)
73
+ assert_invalid_data(/Address type must be/)
74
+ end
75
+
76
+ it "ensures a name for an address exists and has a valid length" do
77
+ data[:creditor][:name] = 'a' * 71
78
+ assert_invalid_data(/Name in address.*70/)
79
+ data[:creditor].delete(:name)
80
+ assert_invalid_data(/Name in address must be provided/)
81
+ end
82
+
83
+ it "ensures that address line 1 has a valid length" do
84
+ data[:creditor][:address_line1] = 'a' * 71
85
+ assert_invalid_data(/Address line 1.*70/)
86
+ end
87
+
88
+ it "ensures a correct country code" do
89
+ data[:creditor][:country] = 'AMS'
90
+ assert_invalid_data(/Country must.*ISO-3166-1/)
91
+ data[:creditor].delete(:country)
92
+ assert_invalid_data(/Country must be provided/)
93
+ end
94
+
95
+ describe "ensures a correct structured address" do
96
+ it "ensures that address line 2 has a valid length" do
97
+ data[:creditor][:address_line2] = 'a' * 17
98
+ assert_invalid_data(/Address line 2.*structured.*16/)
99
+ end
100
+
101
+ it "ensures that the postal code exists and has a valid length" do
102
+ data[:creditor][:postal_code] = 'a' * 17
103
+ assert_invalid_data(/Postal code.*16/)
104
+ data[:creditor].delete(:postal_code)
105
+ assert_invalid_data(/Postal code must be provided.*structured/)
106
+ end
107
+
108
+ it "ensures that the town exists and has a valid length" do
109
+ data[:creditor][:town] = 'a' * 36
110
+ assert_invalid_data(/Town.*35/)
111
+ data[:creditor].delete(:town)
112
+ assert_invalid_data(/Town must be provided.*structured/)
113
+ end
114
+ end
115
+
116
+ describe "ensures a correct combined address" do
117
+ it "ensures that address line 2 exists and has a valid length" do
118
+ data[:debtor][:address_line2] = 'a' * 71
119
+ assert_invalid_data(/Address line 2.*combined.*70/)
120
+ data[:debtor].delete(:address_line2)
121
+ assert_invalid_data(/Address line 2 must be provided.*combined/)
122
+ end
123
+
124
+ it "ensures that the postal code does not exist" do
125
+ data[:debtor][:postal_code] = 'a'
126
+ assert_invalid_data(/Postal code must not be provided.*combined/)
127
+ end
128
+
129
+ it "ensures that the town exists and has a valid length" do
130
+ data[:debtor][:town] = 'a'
131
+ assert_invalid_data(/Town must not be provided.*combined/)
132
+ end
133
+ end
134
+
135
+ describe "reference" do
136
+ it "ensures the QRR reference value exists and is valid" do
137
+ data[:reference_type] = 'QRR'
138
+ assert_invalid_data(/:reference must be filled.*QRR/)
139
+ data[:reference] = 'adsfads'
140
+ assert_invalid_data(/:reference for QRR.*27/)
141
+ data[:reference] = '210000000003139471430009011'
142
+ assert_invalid_data(/:reference for QRR.*invalid check digit.*7/)
143
+ data[:reference] = '21000000000313947143000901'
144
+ assert_equal('7', create_box(data).data[:reference][26])
145
+ data[:reference] = '210000000 0031394 71430009017'
146
+ assert(create_box(data))
147
+ end
148
+
149
+ it "ensures the SCOR reference value exists and is valid" do
150
+ data[:reference_type] = 'SCOR'
151
+ assert_invalid_data(/:reference must be filled.*SCOR/)
152
+ data[:reference] = 'RF11'
153
+ assert_invalid_data(/:reference for SCOR.*5 and 25/)
154
+ data[:reference] = 'RFa' * 9
155
+ assert_invalid_data(/:reference for SCOR.*5 and 25/)
156
+ data[:reference] = 'RF123323;ö'
157
+ assert_invalid_data(/:reference for SCOR.*alpha-numeric/)
158
+ data[:reference] = 'a' * 20
159
+ assert_invalid_data(/:reference for SCOR must start.*RF/)
160
+ data[:reference] = 'RFabcdefgh'
161
+ assert_invalid_data(/:reference for SCOR must start.*RF and check digits/)
162
+ data[:reference] = 'RF 48 5000056789012345d'
163
+ assert_invalid_data(/:reference for SCOR has invalid check digits/)
164
+ data[:reference] = 'RF 48 5000056789012345'
165
+ assert(create_box(data))
166
+ end
167
+
168
+ it "ensures that no reference value is specified for a NON type" do
169
+ data[:reference_type] = 'NON'
170
+ data[:reference] = 'something'
171
+ assert_invalid_data(/:reference must not be provided.*NON/)
172
+ end
173
+
174
+ it "ensures a valid reference_type value" do
175
+ data[:reference_type] = 'OTHER'
176
+ assert_invalid_data(/:reference_type must be one of/)
177
+ end
178
+ end
179
+
180
+ it "ensures a correct message value" do
181
+ data[:message] = 'a' * 141
182
+ assert_invalid_data(/:message must not contain.*140/)
183
+ end
184
+
185
+ it "ensures a correct billing information value" do
186
+ data[:billing_information] = 'a' * 141
187
+ assert_invalid_data(/:billing_information must not contain.*140/)
188
+ end
189
+
190
+ it "ensures a total combined length of message + billing_information of max 140 characters" do
191
+ data[:message] = 'a' * 80
192
+ data[:billing_information] = 'a' * 80
193
+ assert_invalid_data(/:message and :billing_information together.*140/)
194
+ end
195
+
196
+ it "ensures a correct alternative schemes value" do
197
+ data[:alternative_schemes] = 'a' * 101
198
+ assert_invalid_data(/alternative_schemes.*not more than 100/)
199
+ data[:alternative_schemes] = ['a', 'b', 'c']
200
+ assert_invalid_data(/alternative_schemes.*at most contain 2 strings/)
201
+ end
202
+ end
203
+ end
204
+
205
+ describe "draw" do
206
+ before do
207
+ @doc = HexaPDF::Document.new
208
+ @composer = @doc.pages.add.canvas.composer(margin: 0)
209
+ end
210
+
211
+ it "works with all information filled in" do
212
+ data[:reference_type] = 'QRR'
213
+ data[:reference] = '210000000 0031394 71430009017'
214
+ data[:message] = 'Please just pay the bills, Jim!'
215
+ data[:billing_information] = '//S/hit/30/50/what/do/I/do/'
216
+ data[:alternative_schemes] = 'ebill/here/comes/data'
217
+ assert(@composer.box(:swiss_qr_bill, data: data))
218
+ end
219
+
220
+ it "works with no amount and no debtor" do
221
+ data.delete(:debtor)
222
+ data.delete(:amount)
223
+ assert(@composer.box(:swiss_qr_bill, data: data))
224
+ end
225
+
226
+ it "fails if the content is too big for the box" do
227
+ box = create_box(data)
228
+ box.data[:message] = 'x' * 1400
229
+ err = assert_raises(HexaPDF::Error) { @composer.draw_box(box) }
230
+ assert_match(/Swiss QR-bill could not be fit/, err.message)
231
+ end
232
+ end
233
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hexapdf-extras
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Thomas Leitner
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-12-16 00:00:00.000000000 Z
11
+ date: 2024-01-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: hexapdf
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '0.24'
19
+ version: '0.36'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '0.24'
26
+ version: '0.36'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rqrcode_core
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -53,9 +53,11 @@ files:
53
53
  - lib/hexapdf/extras/graphic_object/qr_code.rb
54
54
  - lib/hexapdf/extras/layout.rb
55
55
  - lib/hexapdf/extras/layout/qr_code_box.rb
56
+ - lib/hexapdf/extras/layout/swiss_qr_bill.rb
56
57
  - lib/hexapdf/extras/version.rb
57
58
  - test/hexapdf/extras/graphic_object/test_qr_code.rb
58
59
  - test/hexapdf/extras/layout/test_qr_code_box.rb
60
+ - test/hexapdf/extras/layout/test_swiss_qr_bill.rb
59
61
  - test/test_helper.rb
60
62
  homepage: https://hexapdf-extras.gettalong.org
61
63
  licenses:
@@ -76,7 +78,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
76
78
  - !ruby/object:Gem::Version
77
79
  version: '0'
78
80
  requirements: []
79
- rubygems_version: 3.4.10
81
+ rubygems_version: 3.5.3
80
82
  signing_key:
81
83
  specification_version: 4
82
84
  summary: Additional functionality for HexaPDF