diff --git a/Gemfile b/Gemfile index 1890a3756..4c92bc7d8 100644 --- a/Gemfile +++ b/Gemfile @@ -35,6 +35,8 @@ gem "decidim-user_extension", path: "decidim-user_extension" gem "slack-ruby-client" +gem "ferrum" + group :development, :test do gem "byebug", "~> 11.0", platform: :mri gem "figaro" diff --git a/Gemfile.lock b/Gemfile.lock index abd33be8d..82944f457 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -455,6 +455,11 @@ GEM multipart-post (~> 2) faraday-net_http (3.1.0) net-http + ferrum (0.13) + addressable (~> 2.5) + concurrent-ruby (~> 1.1) + webrick (~> 1.7) + websocket-driver (>= 0.6, < 0.8) ffi (1.16.3) figaro (1.2.0) thor (>= 0.14.0, < 2) @@ -900,6 +905,7 @@ GEM webpush (1.1.0) hkdf (~> 0.2) jwt (~> 2.0) + webrick (1.8.1) websocket-driver (0.7.6) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) @@ -932,6 +938,7 @@ DEPENDENCIES dotenv-rails factory_bot_rails faker (~> 3.2) + ferrum figaro fog-aws image_processing diff --git a/config/initializers/decidim.rb b/config/initializers/decidim.rb index f66e4f8ae..5380b4af7 100644 --- a/config/initializers/decidim.rb +++ b/config/initializers/decidim.rb @@ -490,6 +490,7 @@ Decidim.register_assets_path File.expand_path("app/packs", Rails.application.root) require "decidim/map/provider/static_map/cfj_osm" +require "decidim/exporters/pdf" ## Set default OGP description length limit. It's used in Decidim::Blogs components Rails.application.config.default_blog_ogp_description_limit = ENV.fetch("DECIDIM_BLOG_OGP_DESCRIPTION_LIMIT", 150).to_i diff --git a/lib/decidim/exporters/pdf.rb b/lib/decidim/exporters/pdf.rb new file mode 100644 index 000000000..bee591d62 --- /dev/null +++ b/lib/decidim/exporters/pdf.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +# To be correctly loaded by Zeitwerk +Decidim::Forms::Admin::QuestionnaireAnswerPresenter # rubocop:disable Lint/Void + +module Decidim + module Exporters + # Exports a PDF using the provided hash, given a collection and a + # Serializer. This is an abstract class that should be inherited + # to create PDF exporters, with each PDF exporter class setting + # the desired template, layout and orientation. + # + class PDF < Exporter + # Public: Exports a PDF version of the collection by rendering + # the template into html and then converting it to PDF. + # + # Returns an ExportData instance. + def export + html = controller.render_to_string( + template: template, + layout: layout, + locals: locals + ) + + document = pdf_from_string(html, orientation: orientation) + + ExportData.new(document, "pdf") + end + + # may be overwritten if needed + def orientation + "Portrait" + end + + # implementing classes should return a valid ERB path here + def template + raise NotImplementedError + end + + # implementing classes should return a valid ERB path here + def layout + raise NotImplementedError + end + + # This method may be overwritten if the template needs more local variables + def locals + { collection: collection } + end + + protected + + def controller + raise NotImplementedError + end + + private + + def pdf_from_string(html, orientation:) + document = nil + + Dir.mktmpdir do |dir| + html_path = File.join(dir, "tmp.html") + File.write(html_path, html) + url = URI::File.build([nil, html_path]) + + browser = Ferrum::Browser.new + browser.go_to(url) + document = browser.pdf(path: nil, + encoding: :binary, + landscape: orientation != "Portrait", + printBackground: true, + scale: 0.8, + format: :A4) + end + + document + end + end + end +end