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