diff --git a/.ruby-version b/.ruby-version
index 860487c..49cdd66 100644
--- a/.ruby-version
+++ b/.ruby-version
@@ -1 +1 @@
-2.7.1
+2.7.6
diff --git a/Gemfile.lock b/Gemfile.lock
index dc7ee8c..97d2e4c 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,25 +1,40 @@
PATH
remote: .
specs:
- qr-bills (1.0.7)
+ qr-bills (1.0.9)
i18n (>= 1.8.3, < 2)
+ prawn (>= 1, < 3)
+ prawn-svg
rqrcode (>= 2.1, < 3)
GEM
remote: https://rubygems.org/
specs:
- byebug (11.1.3)
+ addressable (2.8.0)
+ public_suffix (>= 2.0.2, < 5.0)
chunky_png (1.4.0)
coderay (1.1.3)
concurrent-ruby (1.1.10)
+ css_parser (1.11.0)
+ addressable
diff-lcs (1.5.0)
i18n (1.10.0)
concurrent-ruby (~> 1.0)
method_source (1.0.0)
+ pdf-core (0.9.0)
+ prawn (2.4.0)
+ pdf-core (~> 0.9.0)
+ ttfunk (~> 1.7)
+ prawn-svg (0.32.0)
+ css_parser (~> 1.6)
+ prawn (>= 0.11.1, < 3)
+ rexml (~> 3.2)
pry (0.14.1)
coderay (~> 1.1)
method_source (~> 1.0)
+ public_suffix (4.0.7)
rake (13.0.6)
+ rexml (3.2.5)
rqrcode (2.1.1)
chunky_png (~> 1.0)
rqrcode_core (~> 1.0)
@@ -37,12 +52,12 @@ GEM
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.11.0)
rspec-support (3.11.0)
+ ttfunk (1.7.0)
PLATFORMS
ruby
DEPENDENCIES
- byebug
pry
qr-bills!
rake (~> 13.0)
diff --git a/README.md b/README.md
index d0759a1..69bc54c 100644
--- a/README.md
+++ b/README.md
@@ -45,12 +45,15 @@ QRBills.create_creditor_reference("MTR81UUWZYO48NY55NP3")
```ruby
params[:output_params][:format] = "html"
# OR
+params[:output_params][:format] = "prawn"
+# OR
params[:output_params][:format] = "png"
# OR
params[:output_params][:format] = "svg"
```
* `html` returns a full qr-bill as a html-template string, uses `params[:qrcode_format]` for the qrcode format which supports `png` and `svg`. Defaults to `png`.
+* `prawn` returns a full qr-bill in pure Ruby for inclusion in a [Prawn PDF template](https://github.com/prawnpdf/prawn), uses `params[:qrcode_format]` for the qrcode format which supports `svg` ONLY.
* `png` returns the qrcode of the qr-bill as a ChunkyPNG::Image object.
* `svg` returns the qrcode of the qr-bill as a svg string.
@@ -124,6 +127,10 @@ params[:bill_params][:additionally_information] = "pagamento riparazione
# generate the QR Bill
bill = QRBills.generate(params)
+# if params[:output_params][:format] == "prawn" pass `pdf` to QRBills.generate to get a Prawn::Document object
+bill = QRBills.generate(params, pdf)
+
+
# bill format is given in the params, default is html
# bill has the following format:
# bill = {
diff --git a/lib/qr-bills.rb b/lib/qr-bills.rb
index 6602684..5f97401 100644
--- a/lib/qr-bills.rb
+++ b/lib/qr-bills.rb
@@ -2,10 +2,11 @@
require 'qr-bills/qr-exceptions'
require 'qr-bills/qr-params'
require 'qr-bills/qr-html-layout'
+require 'qr-bills/qr-prawn-layout'
require 'qr-bills/qr-creditor-reference'
module QRBills
- def self.generate(qr_params)
+ def self.generate(qr_params, pdf = nil)
raise ArgumentError, "#{QRExceptions::INVALID_PARAMETERS}: bill type param not set" unless qr_params.has_key?(:bill_type)
raise ArgumentError, "#{QRExceptions::INVALID_PARAMETERS}: validation failed" unless QRParams.valid?(qr_params)
@@ -19,6 +20,8 @@ def self.generate(qr_params)
output = case qr_params[:output_params][:format]
when 'html'
QRHTMLLayout.create(qr_params)
+ when 'prawn'
+ QRPRAWNLayout.create(qr_params, pdf)
else
QRGenerator.create(qr_params, qr_params[:qrcode_filepath])
end
diff --git a/lib/qr-bills/qr-prawn-layout.rb b/lib/qr-bills/qr-prawn-layout.rb
new file mode 100644
index 0000000..61a7941
--- /dev/null
+++ b/lib/qr-bills/qr-prawn-layout.rb
@@ -0,0 +1,148 @@
+require 'qr-bills/qr-generator'
+require 'prawn'
+require 'prawn-svg'
+require "prawn/measurement_extensions"
+
+module QRPRAWNLayout
+ attr_reader :document, :type
+
+ def self.create(params, pdf = nil)
+ qrcode = QRGenerator.create(params, params[:qrcode_filepath])
+ params[:qrcode_filepath] = convert_qrcode_to_data_url(qrcode)
+ prawn_layout(params, pdf)
+ end
+
+ def self.convert_qrcode_to_data_url(qrcode)
+ # Stolen from sprockets
+ # https://github.com/rails/sprockets/blob/0f3e0e93dabafa8f3027e8036e40fd08902688c8/lib/sprockets/context.rb#L295-L303
+ data = CGI.escape(qrcode)
+ data.gsub!('%3D', '=')
+ data.gsub!('%3A', ':')
+ data.gsub!('%2F', '/')
+ data.gsub!('%27', "'")
+ data.tr!('+', ' ')
+
+ "data:image/svg+xml;charset=utf-8,#{data}"
+ end
+
+ def self.prawn_layout(params, pdf)
+ pdf.canvas do
+ I18n.with_locale(params[:bill_params][:language]) do
+ pdf.bounding_box([0, 105.mm], width: 210.mm, height: 105.mm) do
+ y_pos = pdf.cursor
+
+ # Receipt Panel
+ pdf.bounding_box([5.mm, y_pos], width: 52.mm, height: 105.mm) do
+ pdf.move_down 5.mm
+
+ pdf.text I18n.t("qrbills.receipt").capitalize, size: 11.pt, style: :bold
+ pdf.move_down 4.mm
+
+ pdf.text "#{I18n.t("qrbills.account").capitalize} / #{I18n.t("qrbills.payable_to").capitalize}", size: 6.pt, style: :bold
+ pdf.text "#{params[:bill_params][:creditor][:iban]}", size: 8.pt
+ pdf.text "#{render_address(params[:bill_params][:creditor][:address])}", size: 8.pt, inline_format: true
+
+ if !params[:bill_params][:reference].nil? && !params[:bill_params][:reference].empty?
+ pdf.move_down 4.mm
+ pdf.text I18n.t("qrbills.reference").capitalize, size: 6.pt, style: :bold
+ pdf.text "#{params[:bill_params][:reference]}", size: 8.pt
+ end
+ pdf.move_down 4.mm
+
+ pdf.text I18n.t("qrbills.payable_by").capitalize, size: 6.pt, style: :bold
+ pdf.text "#{render_address(params[:bill_params][:debtor][:address])}", size: 8.pt, inline_format: true
+ pdf.move_down 8.mm
+
+ bounding_box_cursor = pdf.cursor
+ pdf.bounding_box([0, bounding_box_cursor], width: 20.mm) do
+ pdf.text I18n.t("qrbills.currency").capitalize, size: 6.pt, style: :bold
+ pdf.text "#{params[:bill_params][:currency]}", size: 8.pt
+ end
+
+ pdf.bounding_box([20.mm, bounding_box_cursor], width: 20.mm) do
+ pdf.text I18n.t("qrbills.amount").capitalize, size: 6.pt, style: :bold
+ pdf.text "#{format('%.2f', params[:bill_params][:amount])}", size: 8.pt
+ end
+
+ pdf.move_down 6.mm
+ pdf.text I18n.t("qrbills.acceptance_point"), align: :right, size: 6.pt, style: :bold
+ end
+
+ # Payment Panel - QR code sub-section
+ pdf.bounding_box([67.mm, y_pos], width: 51.mm, height: 90.mm) do
+ pdf.move_down 5.mm
+
+ pdf.text I18n.t("qrbills.payment_part").capitalize, size: 11.pt, style: :bold
+ pdf.move_down 5.mm
+
+ pdf.svg QRGenerator.build_svg(params[:bill_params]), width: 46.mm, height: 46.mm
+ pdf.move_down 5.mm
+
+ payment_currency_bounding_box_cursor = pdf.cursor
+ pdf.bounding_box([0, payment_currency_bounding_box_cursor], width: 20.mm) do
+ pdf.text I18n.t("qrbills.currency").capitalize, size: 8.pt, style: :bold
+ pdf.text "#{params[:bill_params][:currency]}", size: 10.pt
+ end
+
+ pdf.bounding_box([20.mm, payment_currency_bounding_box_cursor], width: 20.mm) do
+ pdf.text I18n.t("qrbills.amount").capitalize, size: 8.pt, style: :bold
+ pdf.text "#{format('%.2f', params[:bill_params][:amount])}", size: 10.pt
+ end
+ end
+
+ # Payment Panel - Account / Payable to sub-section
+ pdf. bounding_box([118.mm, y_pos], width: 92.mm, height: 90.mm) do
+ pdf.move_down 5.mm
+
+ pdf.text "#{I18n.t("qrbills.account").capitalize} / #{I18n.t("qrbills.payable_to").capitalize}", size: 8.pt, style: :bold
+ pdf.text "#{params[:bill_params][:creditor][:iban]}", size: 10.pt
+ pdf.text "#{render_address(params[:bill_params][:creditor][:address])}", size: 10.pt, inline_format: true
+
+ if !params[:bill_params][:reference].nil? && !params[:bill_params][:reference].empty?
+ pdf.move_down 4.mm
+ pdf.text "#{I18n.t("qrbills.reference").capitalize}", size: 8.pt, style: :bold
+ pdf.text "#{params[:bill_params][:reference]}", size: 10.pt
+ end
+
+ if !params[:bill_params][:additionally_information].nil? && !params[:bill_params][:additionally_information].empty?
+ pdf.move_down 4.mm
+ pdf.text "#{I18n.t("qrbills.additional_information").capitalize}", size: 8.pt, style: :bold
+ pdf.text "#{params[:bill_params][:additionally_information]}", size: 10.pt
+ end
+ pdf.move_down 4.mm
+
+ pdf.text "#{I18n.t("qrbills.payable_by").capitalize}", size: 8.pt, style: :bold
+ pdf.text "#{render_address(params[:bill_params][:debtor][:address])}", size: 10.pt, inline_format: true
+ end
+
+ # Payment Panel - Further information sub-section
+ pdf.bounding_box([67.mm, y_pos - 85.mm], width: 138.mm, height: 10.mm) do
+ if !params[:bill_params][:bill_information_coded].nil? && !params[:bill_params][:bill_information_coded].empty?
+ pdf.text "#{I18n.t("qrbills.name").capitalize} AV1: #{params[:bill_params][:bill_information_coded]}", size: 7.pt, inline_format: true
+ end
+
+ if !params[:bill_params][:alternative_scheme_parameters].nil? && !params[:bill_params][:alternative_scheme_parameters].empty?
+ pdf.text "#{I18n.t("qrbills.name").capitalize} AV2: #{params[:bill_params][:alternative_scheme_parameters]}", size: 7.pt, inline_format: true
+ end
+ end
+
+ pdf.stroke_color("808080")
+ pdf.dash(2, space: 2)
+ pdf.stroke_vertical_line 0, 105.mm, at: 62.mm
+ pdf.stroke_horizontal_line 0, 210.mm, at: 105.mm
+ pdf.undash
+
+ end
+ end
+ end
+ end
+
+ def self.render_address(address)
+ case address[:type]
+ when 'S'
+ format("%s
%s %s
%s %s
", address[:name], address[:line1], address[:line2], address[:postal_code], address[:town])
+ when 'K'
+ format("%s
%s
%s
", address[:name], address[:line1], address[:line2])
+ end
+ end
+end
diff --git a/qr-bills.gemspec b/qr-bills.gemspec
index 468f258..a3365fe 100644
--- a/qr-bills.gemspec
+++ b/qr-bills.gemspec
@@ -18,11 +18,13 @@ Gem::Specification.new do |s|
"source_code_uri" => "https://github.com/damoiser/qr-bills",
"wiki_uri" => "https://github.com/damoiser/qr-bills"
}
- s.required_ruby_version = ">= 2.7.1"
+ s.required_ruby_version = ">= 2.7.4"
s.add_runtime_dependency("i18n", ">= 1.8.3", "< 2")
s.add_runtime_dependency("rqrcode", ">= 2.1", "< 3")
+ s.add_runtime_dependency("prawn", ">= 1", "< 3")
+ s.add_runtime_dependency("prawn-svg")
+
s.add_development_dependency("rspec", "~> 3.9")
s.add_development_dependency("rake", "~> 13.0")
s.add_development_dependency("pry")
- s.add_development_dependency("byebug")
end
diff --git a/spec/qr-params_spec.rb b/spec/qr-params_spec.rb
index edad0ce..5b9df65 100644
--- a/spec/qr-params_spec.rb
+++ b/spec/qr-params_spec.rb
@@ -54,15 +54,18 @@
it "fails if currency type is empty" do
@params[:bill_params][:currency] = ""
+ @params[:bill_type] = QRParams::QR_BILL_WITH_QR_REFERENCE
expect{QRParams.base_params_valid?(@params)}.to raise_error(ArgumentError, "QR-bill invalid parameters: currency cannot be blank")
end
it "fails if currency is nil" do
@params[:bill_params][:currency] = nil
+ @params[:bill_type] = QRParams::QR_BILL_WITH_QR_REFERENCE
expect{QRParams.base_params_valid?(@params)}.to raise_error(ArgumentError, "QR-bill invalid parameters: currency cannot be blank")
end
it "succeeds if the previous params are correctly set" do
+ @params[:bill_type] = QRParams::QR_BILL_WITH_QR_REFERENCE
expect{QRParams.base_params_valid?(@params)}.not_to raise_error
expect(QRParams.base_params_valid?(@params)).to be_truthy
end
diff --git a/spec/qr-prawn-layout_spec.rb b/spec/qr-prawn-layout_spec.rb
new file mode 100644
index 0000000..5c8b1b4
--- /dev/null
+++ b/spec/qr-prawn-layout_spec.rb
@@ -0,0 +1,80 @@
+require 'i18n'
+require 'fileutils'
+require 'qr-bills/qr-prawn-layout'
+require 'qr-bills/qr-params'
+
+RSpec.configure do |config|
+ config.before(:each) do
+ @pdf = Prawn::Document.new(:page_size => 'A4')
+ @params = QRParams.get_qr_params
+ @params[:fonts][:eot] = "../web/assets/fonts/LiberationSans-Regular.eot"
+ @params[:fonts][:woff] = "../web/assets/fonts/LiberationSans-Regular.woff"
+ @params[:fonts][:ttf] = "../web/assets/fonts/LiberationSans-Regular.ttf"
+ @params[:fonts][:svg] = "../web/assets/fonts/LiberationSans-Regular.svg"
+ @params[:locales][:path] = "config/locales/"
+ @params[:qrcode_format] = 'svg'
+ @params[:bill_params][:creditor][:iban] = "CH9300762011623852957"
+ @params[:bill_params][:creditor][:address][:type] = "S"
+ @params[:bill_params][:creditor][:address][:name] = "Compagnia di assicurazione forma & scalciante"
+ @params[:bill_params][:creditor][:address][:line1] = "Via cantonale"
+ @params[:bill_params][:creditor][:address][:line2] = "24"
+ @params[:bill_params][:creditor][:address][:postal_code] = "3000"
+ @params[:bill_params][:creditor][:address][:town] = "Lugano"
+ @params[:bill_params][:creditor][:address][:country] = "CH"
+ @params[:bill_params][:amount] = 12345.15
+ @params[:bill_params][:currency] = "CHF"
+ @params[:bill_params][:debtor][:address][:type] = "S"
+ @params[:bill_params][:debtor][:address][:name] = "Foobar Barfoot"
+ @params[:bill_params][:debtor][:address][:line1] = "Via cantonale"
+ @params[:bill_params][:debtor][:address][:line2] = "25"
+ @params[:bill_params][:debtor][:address][:postal_code] = "3001"
+ @params[:bill_params][:debtor][:address][:town] = "Comano"
+ @params[:bill_params][:debtor][:address][:country] = "CH"
+ @params[:bill_params][:reference] = "RF89MTR81UUWZYO48NY55NP3"
+ @params[:bill_params][:reference_type] = "SCOR"
+ @params[:bill_params][:additionally_information] = "pagamento riparazione monopattino"
+
+ I18n.load_path << File.join(@params[:locales][:path], "qrbills.it.yml")
+ I18n.load_path << File.join(@params[:locales][:path], "qrbills.en.yml")
+ I18n.load_path << File.join(@params[:locales][:path], "qrbills.de.yml")
+ I18n.load_path << File.join(@params[:locales][:path], "qrbills.fr.yml")
+ I18n.default_locale = :it
+ end
+end
+
+RSpec.describe "QRPRAWNLayout" do
+ before do
+ FileUtils.mkdir_p "#{Dir.pwd}/tmp/"
+ File.delete filepath if File.exist?(filepath)
+ end
+
+ let(:filepath) { "#{Dir.pwd}/tmp/prawn-layout.pdf" }
+
+ describe "layout generation" do
+ before do
+ @params[:qrcode_format] = 'svg'
+ end
+
+ it "successfully generates prawn/ruby layout + qr code" do
+ expect{QRPRAWNLayout.create(@params, @pdf)}.not_to raise_error
+ end
+
+ it "generates svg qrcode" do
+ expect(@params[:qrcode_filepath]).to_not include("data:image/svg+xml;")
+
+ prawn_output = QRPRAWNLayout.create(@params, @pdf)
+ IO.binwrite(filepath, prawn_output)
+ expect(File.exist?(filepath)).to be_truthy
+
+ expect(@params[:qrcode_filepath]).to include("data:image/svg+xml;")
+ end
+
+ it "does not overwrite locale" do
+ @params[:bill_params][:language] = :de
+
+ QRPRAWNLayout.create(@params, @pdf)
+
+ expect(I18n.locale).to be :it
+ end
+ end
+end