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