hexapdf-extras 1.0.0 → 1.1.0

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: 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: []