From bfa244c8fa87827fae286e474fa015b1c214ea7f Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Mon, 19 May 2025 14:06:37 +0300 Subject: [PATCH 01/15] add ico and bmp support --- lib/load_bmp.rb | 170 ++++++++++++++ lib/load_ico.rb | 209 ++++++++++++++++++ lib/submissions/generate_audit_trail.rb | 2 +- .../generate_result_attachments.rb | 29 ++- 4 files changed, 403 insertions(+), 7 deletions(-) create mode 100644 lib/load_bmp.rb create mode 100644 lib/load_ico.rb diff --git a/lib/load_bmp.rb b/lib/load_bmp.rb new file mode 100644 index 00000000..b5ea074a --- /dev/null +++ b/lib/load_bmp.rb @@ -0,0 +1,170 @@ +# frozen_string_literal: true + +module LoadBmp + module_function + + # rubocop:disable Metrics + def call(bmp_bytes) + bmp_bytes = bmp_bytes.b + + header_data = parse_bmp_headers(bmp_bytes) + + raw_pixel_data_from_file = extract_raw_pixel_data_blob( + bmp_bytes, + header_data[:pixel_data_offset], + header_data[:bmp_stride], + header_data[:height] + ) + + final_pixel_data = prepare_unpadded_pixel_data_string( + raw_pixel_data_from_file, + header_data[:bpp], + header_data[:width], + header_data[:height], + header_data[:bmp_stride] + ) + + bands = header_data[:bpp] / 8 + + unless header_data[:bpp] == 24 || header_data[:bpp] == 32 + raise ArgumentError, "Conversion for #{header_data[:bpp]}-bpp BMP not implemented." + end + + image = Vips::Image.new_from_memory(final_pixel_data, header_data[:width], header_data[:height], bands, :uchar) + + image = image.flip(:vertical) if header_data[:orientation] == -1 + + image_rgb = + if bands == 3 + Vips::Image.bandjoin([image[2], image[1], image[0]]) + elsif bands == 4 + Vips::Image.bandjoin([image[2], image[1], image[0], image[3]]) + else + image + end + + if image_rgb.interpretation == :multiband || image_rgb.interpretation == :'b-w' + image_rgb = image_rgb.copy(interpretation: :srgb) + end + + image_rgb + end + + def parse_bmp_headers(bmp_bytes) + raise ArgumentError, 'BMP data too short for file header (14 bytes).' if bmp_bytes.bytesize < 14 + + signature, pixel_data_offset = bmp_bytes.unpack('a2@10L<') + + raise ArgumentError, "Not a valid BMP file (invalid signature 'BM')." if signature != 'BM' + + raise ArgumentError, 'BMP data too short for info header size field (4 bytes).' if bmp_bytes.bytesize < (14 + 4) + + info_header_size = bmp_bytes.unpack1('@14L<') + + min_expected_info_header_size = 40 + + if info_header_size < min_expected_info_header_size + raise ArgumentError, + "Unsupported BMP info header size: #{info_header_size}. Expected at least #{min_expected_info_header_size}." + end + + header_and_info_header_min_bytes = 14 + min_expected_info_header_size + + if bmp_bytes.bytesize < header_and_info_header_min_bytes + raise ArgumentError, + 'BMP data too short for essential BITMAPINFOHEADER fields ' \ + "(requires #{header_and_info_header_min_bytes} bytes total)." + end + + _header_size_check, width, raw_height_from_header, planes, bpp, compression = + bmp_bytes.unpack('@14L bmp_bytes.bytesize + actual_available = bmp_bytes.bytesize - pixel_data_offset + actual_available = 0 if actual_available.negative? + raise ArgumentError, + "Pixel data segment (offset #{pixel_data_offset}, expected size #{expected_pixel_data_size}) " \ + "exceeds BMP file size (#{bmp_bytes.bytesize}). " \ + "Only #{actual_available} bytes available after offset." + end + + raw_pixel_data_from_file = bmp_bytes.byteslice(pixel_data_offset, expected_pixel_data_size) + + if raw_pixel_data_from_file.nil? || raw_pixel_data_from_file.bytesize < expected_pixel_data_size + raise ArgumentError, + "Extracted pixel data is smaller (#{raw_pixel_data_from_file&.bytesize || 0} bytes) " \ + "than expected (#{expected_pixel_data_size} bytes based on stride and height)." + end + + raw_pixel_data_from_file + end + + def prepare_unpadded_pixel_data_string(raw_pixel_data_from_file, bpp, width, height, bmp_stride) + bytes_per_pixel = bpp / 8 + actual_row_width_bytes = width * bytes_per_pixel + + unpadded_rows = Array.new(height) + current_offset_in_blob = 0 + + height.times do |i| + if current_offset_in_blob + actual_row_width_bytes > raw_pixel_data_from_file.bytesize + raise ArgumentError, + "Not enough data in pixel blob for row #{i}. Offset #{current_offset_in_blob}, " \ + "row width #{actual_row_width_bytes}, blob size #{raw_pixel_data_from_file.bytesize}" + end + + unpadded_row_slice = raw_pixel_data_from_file.byteslice(current_offset_in_blob, actual_row_width_bytes) + + if unpadded_row_slice.nil? || unpadded_row_slice.bytesize < actual_row_width_bytes + raise ArgumentError, "Failed to slice a full unpadded row from pixel data blob for row #{i}." + end + + unpadded_rows[i] = unpadded_row_slice + current_offset_in_blob += bmp_stride + end + + unpadded_rows.join + end + # rubocop:enable Metrics +end diff --git a/lib/load_ico.rb b/lib/load_ico.rb new file mode 100644 index 00000000..2c48d766 --- /dev/null +++ b/lib/load_ico.rb @@ -0,0 +1,209 @@ +# frozen_string_literal: true + +module LoadIco + BI_RGB = 0 + + module_function + + # rubocop:disable Metrics + def call(ico_bytes) + io = StringIO.new(ico_bytes) + _reserved, type, count = io.read(6)&.unpack('S= 40 + + dib_params_bytes = dib_io.read(36) + return nil unless dib_params_bytes && dib_params_bytes.bytesize == 36 + + dib_width, dib_actual_height_field, dib_planes, dib_bpp, + dib_compression, _dib_image_size, _xpels, _ypels, + dib_clr_used, _dib_clr_important = dib_params_bytes.unpack('l= min_xor_bytes_needed + + and_mask_bits_for_row = [] + if has_and_mask + dib_io.seek(and_mask_data_offset + (y_dib_row * and_scanline_stride)) + and_mask_scanline_bytes = dib_io.read(and_scanline_stride) + min_and_bytes_needed = ((dib_width * 1) + 7) / 8 + return nil unless and_mask_scanline_bytes && and_mask_scanline_bytes.bytesize >= min_and_bytes_needed + + (0...dib_width).each do |x_pixel| + byte_index = x_pixel / 8 + bit_index_in_byte = 7 - (x_pixel % 8) + byte_val = and_mask_scanline_bytes.getbyte(byte_index) + and_mask_bits_for_row << ((byte_val >> bit_index_in_byte) & 1) + end + end + + (0...dib_width).each do |x_pixel| + r = 0 + g = 0 + b = 0 + a = 255 + + case dib_bpp + when 32 + offset = x_pixel * 4 + blue = xor_scanline_bytes.getbyte(offset) + green = xor_scanline_bytes.getbyte(offset + 1) + red = xor_scanline_bytes.getbyte(offset + 2) + alpha_val = xor_scanline_bytes.getbyte(offset + 3) + r = red + g = green + b = blue + a = alpha_val + when 24 + offset = x_pixel * 3 + blue = xor_scanline_bytes.getbyte(offset) + green = xor_scanline_bytes.getbyte(offset + 1) + red = xor_scanline_bytes.getbyte(offset + 2) + r = red + g = green + b = blue + when 8 + idx = xor_scanline_bytes.getbyte(x_pixel) + r_p, g_p, b_p, a_p = palette[idx] || [0, 0, 0, 0] + r = r_p + g = g_p + b = b_p + a = a_p + when 4 + byte_val = xor_scanline_bytes.getbyte(x_pixel / 2) + idx = (x_pixel.even? ? (byte_val >> 4) : (byte_val & 0x0F)) + r_p, g_p, b_p, a_p = palette[idx] || [0, 0, 0, 0] + r = r_p + g = g_p + b = b_p + a = a_p + when 1 + byte_val = xor_scanline_bytes.getbyte(x_pixel / 8) + idx = (byte_val >> (7 - (x_pixel % 8))) & 1 + r_p, g_p, b_p, a_p = palette[idx] || [0, 0, 0, 0] + r = r_p + g = g_p + b = b_p + a = a_p + end + + if has_and_mask && !and_mask_bits_for_row.empty? + a = and_mask_bits_for_row[x_pixel] == 1 ? 0 : 255 + end + flat_rgba_pixels.push(r, g, b, a) + end + end + + pixel_data_string = flat_rgba_pixels.pack('C*') + + expected_bytes = dib_width * image_pixel_height * 4 + + return nil unless pixel_data_string.bytesize == expected_bytes && expected_bytes.positive? + + Vips::Image.new_from_memory( + pixel_data_string, + dib_width, + image_pixel_height, + 4, + :uchar + ) + end + # rubocop:enable Metrics +end diff --git a/lib/submissions/generate_audit_trail.rb b/lib/submissions/generate_audit_trail.rb index ecfe9b84..32622049 100644 --- a/lib/submissions/generate_audit_trail.rb +++ b/lib/submissions/generate_audit_trail.rb @@ -309,7 +309,7 @@ module Submissions image = begin - Vips::Image.new_from_buffer(attachment.download, '').autorot + Submissions::GenerateResultAttachments.load_vips_image(attachment).autorot rescue Vips::Error next unless attachment.content_type.starts_with?('image/') next if attachment.byte_size.zero? diff --git a/lib/submissions/generate_result_attachments.rb b/lib/submissions/generate_result_attachments.rb index 0a5da813..de8e055f 100644 --- a/lib/submissions/generate_result_attachments.rb +++ b/lib/submissions/generate_result_attachments.rb @@ -11,6 +11,9 @@ module Submissions 'Helvetica' end + ICO_REGEXP = %r{\Aimage/(?:x-icon|vnd\.microsoft\.icon)\z} + BMP_REGEXP = %r{\Aimage/(?:bmp|x-bmp|x-ms-bmp)\z} + FONT_BOLD_NAME = if File.exist?(FONT_BOLD_PATH) FONT_BOLD_PATH else @@ -250,9 +253,7 @@ module Submissions when ->(type) { type == 'signature' && (with_signature_id || field.dig('preferences', 'reason_field_uuid')) } attachment = submitter.attachments.find { |a| a.uuid == value } - attachments_data_cache[attachment.uuid] ||= attachment.download - - image = Vips::Image.new_from_buffer(attachments_data_cache[attachment.uuid], '').autorot + image = load_vips_image(attachment, attachments_data_cache).autorot reason_value = submitter.values[field.dig('preferences', 'reason_field_uuid')].presence @@ -360,11 +361,9 @@ module Submissions when 'image', 'signature', 'initials', 'stamp' attachment = submitter.attachments.find { |a| a.uuid == value } - attachments_data_cache[attachment.uuid] ||= attachment.download - image = begin - Vips::Image.new_from_buffer(attachments_data_cache[attachment.uuid], '').autorot + load_vips_image(attachment, attachments_data_cache).autorot rescue Vips::Error next unless attachment.content_type.starts_with?('image/') next if attachment.byte_size.zero? @@ -804,6 +803,24 @@ module Submissions [] end + def load_vips_image(attachment, cache = {}) + cache[attachment.uuid] ||= attachment.download + + data = cache[attachment.uuid] + + if ICO_REGEXP.match?(attachment.content_type) + Rollbar.error("Load ICO: #{attachment.uuid}") if defined?(Rollbar) + + LoadIco.call(data) + elsif BMP_REGEXP.match?(attachment.content_type) + Rollbar.error("Load BMP: #{attachment.uuid}") if defined?(Rollbar) + + LoadBmp.call(data) + else + Vips::Image.new_from_buffer(data, '') + end + end + def h Rails.application.routes.url_helpers end From 047c1929888174794b4a4c9e0ae542d94fe5817e Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Tue, 20 May 2025 08:21:20 +0300 Subject: [PATCH 02/15] fix typo --- config/locales/i18n.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config/locales/i18n.yml b/config/locales/i18n.yml index 15ecb073..591b1bc7 100644 --- a/config/locales/i18n.yml +++ b/config/locales/i18n.yml @@ -46,7 +46,7 @@ en: &en team_member_permissions: Team member permissions entire_team: Entire team admin_only: Admin only - accessiable_by: Accessiable by + accessiable_by: Accessible by team_access: Team access document_download_filename_format: Document download filename format document_name: Document Name @@ -655,7 +655,7 @@ en: &en completed_at: Completed at edit_recipient: Edit Recipient update_recipient: Update Recipient - use_international_format_1xxx_: 'Use internatioanl format: +1xxx...' + use_international_format_1xxx_: 'Use international format: +1xxx...' submitter_cannot_be_updated: Submitter cannot be updated. at_least_one_field_must_be_filled: At least one field must be filled. archived_users: Archived Users @@ -745,7 +745,7 @@ en: &en api: API bulk: Bulk Send embed: Embedding - invie: Invite + invite: Invite link: Link submission_event_names: send_email_to_html: 'Email sent to %{submitter_name}' From 14fa8fc8e74d09aa60ce9b9057a9bcca5419495d Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Tue, 20 May 2025 11:12:31 +0300 Subject: [PATCH 03/15] fix expired --- app/models/submission.rb | 2 +- app/views/templates/_submission.html.erb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/submission.rb b/app/models/submission.rb index ef40ef68..f448adcd 100644 --- a/app/models/submission.rb +++ b/app/models/submission.rb @@ -71,7 +71,7 @@ class Submission < ApplicationRecord .and(Submitter.arel_table[:completed_at].eq(nil))).select(1).arel.exists) } scope :declined, -> { joins(:submitters).where.not(submitters: { declined_at: nil }).group(:id) } - scope :expired, -> { where(expire_at: ..Time.current) } + scope :expired, -> { pending.where(expire_at: ..Time.current) } enum :source, { invite: 'invite', diff --git a/app/views/templates/_submission.html.erb b/app/views/templates/_submission.html.erb index f7bc7a83..5d75a047 100644 --- a/app/views/templates/_submission.html.erb +++ b/app/views/templates/_submission.html.erb @@ -34,7 +34,7 @@ <% submitter = submitters.first %>
- <% if submission.expired? %> + <% if submission.expired? && !submitter.completed_at? && !submitter.declined_at? %>
<%= t('expired') %> From 0fb9c2bbf6c706b307824caebc43383a458cde55 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Tue, 20 May 2025 19:32:23 +0300 Subject: [PATCH 04/15] fix gmail normalize --- lib/submissions.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/submissions.rb b/lib/submissions.rb index 90ebfad4..92e715ed 100644 --- a/lib/submissions.rb +++ b/lib/submissions.rb @@ -120,6 +120,8 @@ module Submissions email = email.to_s.tr('/', ',') + return email.downcase.sub(/@gmail?\z/i, '@gmail.com') if email.match?(/@gmail?\z/i) + return email.downcase if email.include?(',') || email.match?(/\.(?:gob|om|mm|cm|et|mo|nz|za|ie)\z/) || email.exclude?('.') From 23631b75f285f95b794b7c6da5eef27d085eca52 Mon Sep 17 00:00:00 2001 From: Alex Turchyn Date: Mon, 19 May 2025 13:09:29 +0300 Subject: [PATCH 05/15] add template_accesses factory --- spec/factories/template_accesses.rb | 8 ++++++++ spec/factories/templates.rb | 13 +++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 spec/factories/template_accesses.rb diff --git a/spec/factories/template_accesses.rb b/spec/factories/template_accesses.rb new file mode 100644 index 00000000..9c33173e --- /dev/null +++ b/spec/factories/template_accesses.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :template_access do + template + user + end +end diff --git a/spec/factories/templates.rb b/spec/factories/templates.rb index 8f619353..2091821c 100644 --- a/spec/factories/templates.rb +++ b/spec/factories/templates.rb @@ -14,6 +14,7 @@ FactoryBot.define do %w[text date checkbox radio signature number multiple select initials image file stamp cells phone payment] end except_field_types { [] } + private_access_user { nil } end after(:create) do |template, ev| @@ -343,5 +344,17 @@ FactoryBot.define do template.save! end + + trait :with_admin_only_access do + after(:create) do |template| + create(:template_access, template:, user_id: TemplateAccess::ADMIN_USER_ID) + end + end + + trait :with_private_access do + after(:create) do |template, ev| + create(:template_access, template:, user: ev.private_access_user || template.author) + end + end end end From 91a96f06c8582be0a5cfdb1c854edc37d8d28ad0 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Tue, 20 May 2025 22:41:47 +0300 Subject: [PATCH 06/15] remove account config action --- app/controllers/account_configs_controller.rb | 14 ++++++++++++-- config/locales/i18n.yml | 18 ++++++++++++++++++ config/routes.rb | 2 +- 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/app/controllers/account_configs_controller.rb b/app/controllers/account_configs_controller.rb index 4146c130..e868d08a 100644 --- a/app/controllers/account_configs_controller.rb +++ b/app/controllers/account_configs_controller.rb @@ -1,8 +1,10 @@ # frozen_string_literal: true class AccountConfigsController < ApplicationController - before_action :load_account_config - authorize_resource :account_config + before_action :load_account_config, only: :create + authorize_resource :account_config, only: :create + + load_and_authorize_resource :account_config, only: :destroy ALLOWED_KEYS = [ AccountConfig::ALLOW_TYPED_SIGNATURE, @@ -30,6 +32,14 @@ class AccountConfigsController < ApplicationController head :ok end + def destroy + raise InvalidKey unless ALLOWED_KEYS.include?(@account_config.key) + + @account_config.destroy! + + redirect_back(fallback_location: root_path) + end + private def load_account_config diff --git a/config/locales/i18n.yml b/config/locales/i18n.yml index 591b1bc7..099dfb02 100644 --- a/config/locales/i18n.yml +++ b/config/locales/i18n.yml @@ -22,6 +22,9 @@ en: &en hi_there: Hi there thanks: Thanks private: Private + stripe_integration: Stripe Integration + stripe_account_has_been_connected: Stripe account has been connected. + re_connect_stripe: Re-connect Stripe bcc_recipients: BCC recipients resend_pending: Re-send pending always_enforce_signing_order: Always enforce the signing order @@ -827,6 +830,9 @@ en: &en read: Read your data es: &es + stripe_integration: Integración con Stripe + stripe_account_has_been_connected: La cuenta de Stripe ha sido conectada. + re_connect_stripe: Volver a conectar Stripe private: Privado resend_pending: Reenviar pendiente ensure_unique_recipients: Asegurar destinatarios únicos @@ -1635,6 +1641,9 @@ es: &es read: Leer tus datos it: &it + stripe_integration: Integrazione Stripe + stripe_account_has_been_connected: L'account Stripe è stato collegato. + re_connect_stripe: Ricollega Stripe private: Privato resend_pending: Reinvia in sospeso ensure_unique_recipients: Assicurarsi destinatari unici @@ -2441,6 +2450,9 @@ it: &it read: Leggi i tuoi dati fr: &fr + stripe_integration: Intégration Stripe + stripe_account_has_been_connected: Le compte Stripe a été connecté. + re_connect_stripe: Reconnecter Stripe private: Privé resend_pending: Renvoyer en attente ensure_unique_recipients: Assurer l'unicité des destinataires @@ -3250,6 +3262,9 @@ fr: &fr read: Lire vos données pt: &pt + stripe_integration: Integração com Stripe + stripe_account_has_been_connected: Conta Stripe foi conectada. + re_connect_stripe: Reconectar Stripe private: Privado resend_pending: Re-enviar pendente ensure_unique_recipients: Garantir destinatários únicos @@ -4059,6 +4074,9 @@ pt: &pt read: Ler seus dados de: &de + stripe_integration: Stripe-Integration + stripe_account_has_been_connected: Stripe-Konto wurde verbunden. + re_connect_stripe: Stripe erneut verbinden private: Privat resend_pending: Ausstehende erneut senden ensure_unique_recipients: Stellen Sie einzigartige Empfänger sicher diff --git a/config/routes.rb b/config/routes.rb index ba05e364..331ca82d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -57,7 +57,7 @@ Rails.application.routes.draw do resources :verify_pdf_signature, only: %i[create] resource :mfa_setup, only: %i[show new edit create destroy], controller: 'mfa_setup' - resources :account_configs, only: %i[create] + resources :account_configs, only: %i[create destroy] resources :user_configs, only: %i[create] resources :encrypted_user_configs, only: %i[destroy] resources :timestamp_server, only: %i[create] From a7ed0b86a45f87038c889d4d636235e0a09c34e8 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Wed, 21 May 2025 09:48:17 +0300 Subject: [PATCH 07/15] dot rspec --- .rspec | 1 + spec/jobs/process_submitter_completion_job_spec.rb | 2 -- spec/jobs/send_form_completed_webhook_request_job_spec.rb | 2 -- spec/jobs/send_form_declined_webhook_request_job_spec.rb | 2 -- spec/jobs/send_form_started_webhook_request_job_spec.rb | 2 -- spec/jobs/send_form_viewed_webhook_request_job_spec.rb | 2 -- .../send_submission_archived_webhook_request_job_spec.rb | 2 -- .../send_submission_completed_webhook_request_job_spec.rb | 2 -- .../send_submission_created_webhook_request_job_spec.rb | 2 -- spec/jobs/send_template_created_webhook_request_job_spec.rb | 2 -- spec/jobs/send_template_updated_webhook_request_job_spec.rb | 2 -- spec/lib/params/base_validator_spec.rb | 2 -- spec/requests/submissions_spec.rb | 4 +--- spec/requests/submitters_spec.rb | 4 +--- spec/requests/templates_spec.rb | 4 +--- spec/requests/tools_spec.rb | 4 +--- spec/system/account_settings_spec.rb | 2 -- spec/system/api_settings_spec.rb | 2 -- spec/system/dashboard_spec.rb | 2 -- spec/system/email_settings_spec.rb | 2 -- spec/system/esign_spec.rb | 2 -- spec/system/newsletters_spec.rb | 2 -- spec/system/notifications_settings_spec.rb | 2 -- spec/system/personalization_settings_spec.rb | 2 -- spec/system/personalization_spec.rb | 2 -- spec/system/profile_settings_spec.rb | 2 -- spec/system/setup_spec.rb | 2 -- spec/system/sign_in_spec.rb | 4 +--- spec/system/signing_form_spec.rb | 6 ++---- spec/system/storage_settings_spec.rb | 2 -- spec/system/submission_preview_spec.rb | 2 -- spec/system/team_settings_spec.rb | 2 -- spec/system/template_builder_spec.rb | 2 -- spec/system/template_spec.rb | 2 -- spec/system/webhook_settings_spec.rb | 2 -- 35 files changed, 8 insertions(+), 75 deletions(-) create mode 100644 .rspec diff --git a/.rspec b/.rspec new file mode 100644 index 00000000..a35c44f4 --- /dev/null +++ b/.rspec @@ -0,0 +1 @@ +--require rails_helper diff --git a/spec/jobs/process_submitter_completion_job_spec.rb b/spec/jobs/process_submitter_completion_job_spec.rb index 632003b3..cb357efc 100644 --- a/spec/jobs/process_submitter_completion_job_spec.rb +++ b/spec/jobs/process_submitter_completion_job_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'rails_helper' - RSpec.describe ProcessSubmitterCompletionJob do let(:account) { create(:account) } let(:user) { create(:user, account:) } diff --git a/spec/jobs/send_form_completed_webhook_request_job_spec.rb b/spec/jobs/send_form_completed_webhook_request_job_spec.rb index b57e74b2..cf09d1e1 100644 --- a/spec/jobs/send_form_completed_webhook_request_job_spec.rb +++ b/spec/jobs/send_form_completed_webhook_request_job_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'rails_helper' - RSpec.describe SendFormCompletedWebhookRequestJob do let(:account) { create(:account) } let(:user) { create(:user, account:) } diff --git a/spec/jobs/send_form_declined_webhook_request_job_spec.rb b/spec/jobs/send_form_declined_webhook_request_job_spec.rb index c53a1999..1dcfb181 100644 --- a/spec/jobs/send_form_declined_webhook_request_job_spec.rb +++ b/spec/jobs/send_form_declined_webhook_request_job_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'rails_helper' - RSpec.describe SendFormDeclinedWebhookRequestJob do let(:account) { create(:account) } let(:user) { create(:user, account:) } diff --git a/spec/jobs/send_form_started_webhook_request_job_spec.rb b/spec/jobs/send_form_started_webhook_request_job_spec.rb index 929a2187..77cd5bdd 100644 --- a/spec/jobs/send_form_started_webhook_request_job_spec.rb +++ b/spec/jobs/send_form_started_webhook_request_job_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'rails_helper' - RSpec.describe SendFormStartedWebhookRequestJob do let(:account) { create(:account) } let(:user) { create(:user, account:) } diff --git a/spec/jobs/send_form_viewed_webhook_request_job_spec.rb b/spec/jobs/send_form_viewed_webhook_request_job_spec.rb index c3c7c216..c182af0d 100644 --- a/spec/jobs/send_form_viewed_webhook_request_job_spec.rb +++ b/spec/jobs/send_form_viewed_webhook_request_job_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'rails_helper' - RSpec.describe SendFormViewedWebhookRequestJob do let(:account) { create(:account) } let(:user) { create(:user, account:) } diff --git a/spec/jobs/send_submission_archived_webhook_request_job_spec.rb b/spec/jobs/send_submission_archived_webhook_request_job_spec.rb index 05947f33..233f1661 100644 --- a/spec/jobs/send_submission_archived_webhook_request_job_spec.rb +++ b/spec/jobs/send_submission_archived_webhook_request_job_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'rails_helper' - RSpec.describe SendSubmissionArchivedWebhookRequestJob do let(:account) { create(:account) } let(:user) { create(:user, account:) } diff --git a/spec/jobs/send_submission_completed_webhook_request_job_spec.rb b/spec/jobs/send_submission_completed_webhook_request_job_spec.rb index e2f0d26e..3a7053df 100644 --- a/spec/jobs/send_submission_completed_webhook_request_job_spec.rb +++ b/spec/jobs/send_submission_completed_webhook_request_job_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'rails_helper' - RSpec.describe SendSubmissionCompletedWebhookRequestJob do let(:account) { create(:account) } let(:user) { create(:user, account:) } diff --git a/spec/jobs/send_submission_created_webhook_request_job_spec.rb b/spec/jobs/send_submission_created_webhook_request_job_spec.rb index 1b2e02fe..1decf1b0 100644 --- a/spec/jobs/send_submission_created_webhook_request_job_spec.rb +++ b/spec/jobs/send_submission_created_webhook_request_job_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'rails_helper' - RSpec.describe SendSubmissionCreatedWebhookRequestJob do let(:account) { create(:account) } let(:user) { create(:user, account:) } diff --git a/spec/jobs/send_template_created_webhook_request_job_spec.rb b/spec/jobs/send_template_created_webhook_request_job_spec.rb index 14df0b3e..bb2b51c6 100644 --- a/spec/jobs/send_template_created_webhook_request_job_spec.rb +++ b/spec/jobs/send_template_created_webhook_request_job_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'rails_helper' - RSpec.describe SendTemplateCreatedWebhookRequestJob do let(:account) { create(:account) } let(:user) { create(:user, account:) } diff --git a/spec/jobs/send_template_updated_webhook_request_job_spec.rb b/spec/jobs/send_template_updated_webhook_request_job_spec.rb index 4451b071..2edcd280 100644 --- a/spec/jobs/send_template_updated_webhook_request_job_spec.rb +++ b/spec/jobs/send_template_updated_webhook_request_job_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'rails_helper' - RSpec.describe SendTemplateUpdatedWebhookRequestJob do let(:account) { create(:account) } let(:user) { create(:user, account:) } diff --git a/spec/lib/params/base_validator_spec.rb b/spec/lib/params/base_validator_spec.rb index 5a00e188..7021a6b3 100644 --- a/spec/lib/params/base_validator_spec.rb +++ b/spec/lib/params/base_validator_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'rails_helper' - RSpec.describe Params::BaseValidator do let(:validator) { described_class.new({}) } diff --git a/spec/requests/submissions_spec.rb b/spec/requests/submissions_spec.rb index 020bd908..5f9e98db 100644 --- a/spec/requests/submissions_spec.rb +++ b/spec/requests/submissions_spec.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require 'rails_helper' - -describe 'Submission API', type: :request do +describe 'Submission API' do let(:account) { create(:account, :with_testing_account) } let(:testing_account) { account.testing_accounts.first } let(:author) { create(:user, account:) } diff --git a/spec/requests/submitters_spec.rb b/spec/requests/submitters_spec.rb index 023de7ee..faa7dd4d 100644 --- a/spec/requests/submitters_spec.rb +++ b/spec/requests/submitters_spec.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require 'rails_helper' - -describe 'Submitter API', type: :request do +describe 'Submitter API' do let(:account) { create(:account, :with_testing_account) } let(:testing_account) { account.testing_accounts.first } let(:author) { create(:user, account:) } diff --git a/spec/requests/templates_spec.rb b/spec/requests/templates_spec.rb index 3d6e1284..fa2a362a 100644 --- a/spec/requests/templates_spec.rb +++ b/spec/requests/templates_spec.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require 'rails_helper' - -describe 'Templates API', type: :request do +describe 'Templates API' do let(:account) { create(:account, :with_testing_account) } let(:testing_account) { account.testing_accounts.first } let(:author) { create(:user, account:) } diff --git a/spec/requests/tools_spec.rb b/spec/requests/tools_spec.rb index b51f771c..3de12fcc 100644 --- a/spec/requests/tools_spec.rb +++ b/spec/requests/tools_spec.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require 'rails_helper' - -describe 'Tools API', type: :request do +describe 'Tools API' do let(:account) { create(:account) } let(:author) { create(:user, account:) } let(:file_path) { Rails.root.join('spec/fixtures/sample-document.pdf') } diff --git a/spec/system/account_settings_spec.rb b/spec/system/account_settings_spec.rb index ae3bef28..7c194219 100644 --- a/spec/system/account_settings_spec.rb +++ b/spec/system/account_settings_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'rails_helper' - RSpec.describe 'Account Settings' do let!(:account) { create(:account) } let!(:user) { create(:user, account:) } diff --git a/spec/system/api_settings_spec.rb b/spec/system/api_settings_spec.rb index 648e202d..a14ef36d 100644 --- a/spec/system/api_settings_spec.rb +++ b/spec/system/api_settings_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'rails_helper' - RSpec.describe 'API Settings' do let!(:account) { create(:account) } let!(:user) { create(:user, account:) } diff --git a/spec/system/dashboard_spec.rb b/spec/system/dashboard_spec.rb index f2bd11b1..81d16458 100644 --- a/spec/system/dashboard_spec.rb +++ b/spec/system/dashboard_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'rails_helper' - RSpec.describe 'Dashboard Page' do let!(:account) { create(:account) } let!(:user) { create(:user, account:) } diff --git a/spec/system/email_settings_spec.rb b/spec/system/email_settings_spec.rb index 5e2a00e5..f7d683b8 100644 --- a/spec/system/email_settings_spec.rb +++ b/spec/system/email_settings_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'rails_helper' - RSpec.describe 'Email Settings' do let!(:account) { create(:account) } let!(:user) { create(:user, account:) } diff --git a/spec/system/esign_spec.rb b/spec/system/esign_spec.rb index b1a48f62..d7441d7e 100644 --- a/spec/system/esign_spec.rb +++ b/spec/system/esign_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'rails_helper' - RSpec.describe 'PDF Signature Settings' do let!(:account) { create(:account) } let!(:user) { create(:user, account:) } diff --git a/spec/system/newsletters_spec.rb b/spec/system/newsletters_spec.rb index a4fc3c18..274ba7e3 100644 --- a/spec/system/newsletters_spec.rb +++ b/spec/system/newsletters_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'rails_helper' - RSpec.describe 'Newsletter' do let(:user) { create(:user, account: create(:account)) } diff --git a/spec/system/notifications_settings_spec.rb b/spec/system/notifications_settings_spec.rb index 4f0d0f7c..2edd3eb5 100644 --- a/spec/system/notifications_settings_spec.rb +++ b/spec/system/notifications_settings_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'rails_helper' - RSpec.describe 'Notifications Settings' do let(:user) { create(:user, account: create(:account)) } diff --git a/spec/system/personalization_settings_spec.rb b/spec/system/personalization_settings_spec.rb index 9d16d0c0..8b7fa214 100644 --- a/spec/system/personalization_settings_spec.rb +++ b/spec/system/personalization_settings_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'rails_helper' - RSpec.describe 'Personalization Settings', :js do let!(:account) { create(:account) } let!(:user) { create(:user, account:) } diff --git a/spec/system/personalization_spec.rb b/spec/system/personalization_spec.rb index f82173a1..18aa04b1 100644 --- a/spec/system/personalization_spec.rb +++ b/spec/system/personalization_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'rails_helper' - RSpec.describe 'Personalization' do let!(:account) { create(:account) } let!(:user) { create(:user, account:) } diff --git a/spec/system/profile_settings_spec.rb b/spec/system/profile_settings_spec.rb index 351b636c..7b977d2d 100644 --- a/spec/system/profile_settings_spec.rb +++ b/spec/system/profile_settings_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'rails_helper' - RSpec.describe 'Profile Settings' do let(:user) { create(:user, account: create(:account)) } diff --git a/spec/system/setup_spec.rb b/spec/system/setup_spec.rb index 25c6df27..5235c9b5 100644 --- a/spec/system/setup_spec.rb +++ b/spec/system/setup_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'rails_helper' - RSpec.describe 'App Setup' do let(:form_data) do { diff --git a/spec/system/sign_in_spec.rb b/spec/system/sign_in_spec.rb index e9e2f4d1..df0e4f02 100644 --- a/spec/system/sign_in_spec.rb +++ b/spec/system/sign_in_spec.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require 'rails_helper' - -RSpec.describe 'Sign In', type: :system do +RSpec.describe 'Sign In' do let(:account) { create(:account) } let!(:user) { create(:user, account:, email: 'john.dou@example.com', password: 'strong_password') } diff --git a/spec/system/signing_form_spec.rb b/spec/system/signing_form_spec.rb index 300edc2e..8bfa3e5e 100644 --- a/spec/system/signing_form_spec.rb +++ b/spec/system/signing_form_spec.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require 'rails_helper' - -RSpec.describe 'Signing Form', type: :system do +RSpec.describe 'Signing Form' do let(:account) { create(:account) } let(:author) { create(:user, account:) } @@ -13,7 +11,7 @@ RSpec.describe 'Signing Form', type: :system do visit start_form_path(slug: template.slug) end - it 'shows the email step', type: :system do + it 'shows the email step' do expect(page).to have_content('You have been invited to submit a form') expect(page).to have_content("Invited by #{account.name}") expect(page).to have_field('Email', type: 'email') diff --git a/spec/system/storage_settings_spec.rb b/spec/system/storage_settings_spec.rb index d57564d7..c11d0ea0 100644 --- a/spec/system/storage_settings_spec.rb +++ b/spec/system/storage_settings_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'rails_helper' - RSpec.describe 'Storage Settings' do let!(:account) { create(:account) } let!(:user) { create(:user, account:) } diff --git a/spec/system/submission_preview_spec.rb b/spec/system/submission_preview_spec.rb index b231fcff..54fb75f2 100644 --- a/spec/system/submission_preview_spec.rb +++ b/spec/system/submission_preview_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'rails_helper' - RSpec.describe 'Submission Preview' do let(:account) { create(:account) } let(:user) { create(:user, account:) } diff --git a/spec/system/team_settings_spec.rb b/spec/system/team_settings_spec.rb index 077c5104..0286267f 100644 --- a/spec/system/team_settings_spec.rb +++ b/spec/system/team_settings_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'rails_helper' - RSpec.describe 'Team Settings' do let(:account) { create(:account) } let(:second_account) { create(:account) } diff --git a/spec/system/template_builder_spec.rb b/spec/system/template_builder_spec.rb index d14ce3b9..2f4b9456 100644 --- a/spec/system/template_builder_spec.rb +++ b/spec/system/template_builder_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'rails_helper' - RSpec.describe 'Template Builder' do let(:account) { create(:account) } let(:author) { create(:user, account:) } diff --git a/spec/system/template_spec.rb b/spec/system/template_spec.rb index 0b512041..a0ffe4e5 100644 --- a/spec/system/template_spec.rb +++ b/spec/system/template_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'rails_helper' - RSpec.describe 'Template' do let!(:account) { create(:account) } let!(:user) { create(:user, account:) } diff --git a/spec/system/webhook_settings_spec.rb b/spec/system/webhook_settings_spec.rb index fbbcaea2..5b4cbaf2 100644 --- a/spec/system/webhook_settings_spec.rb +++ b/spec/system/webhook_settings_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'rails_helper' - RSpec.describe 'Webhook Settings' do let!(:account) { create(:account) } let!(:user) { create(:user, account:) } From 38674a9fc949ab0cc37cc5c98055ba7d77b3997a Mon Sep 17 00:00:00 2001 From: Alex Turchyn Date: Wed, 21 May 2025 22:05:33 +0300 Subject: [PATCH 08/15] fix accidental checkbox addition --- app/javascript/template_builder/builder.vue | 4 ++-- app/javascript/template_builder/document.vue | 2 +- app/javascript/template_builder/page.vue | 7 ++++++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/app/javascript/template_builder/builder.vue b/app/javascript/template_builder/builder.vue index 514740eb..ab26e912 100644 --- a/app/javascript/template_builder/builder.vue +++ b/app/javascript/template_builder/builder.vue @@ -1279,7 +1279,7 @@ export default { area.h = (pageMask.clientWidth / 35 / pageMask.clientWidth) } }, - onDraw (area) { + onDraw ({ area, isTooSmall }) { if (this.drawField) { if (this.drawOption) { const areaWithoutOption = this.drawField.areas?.find((a) => !a.option_uuid) @@ -1374,7 +1374,7 @@ export default { area.y -= area.h / 2 } - if (area.w) { + if (area.w && (type !== 'checkbox' || this.drawFieldType || !isTooSmall)) { this.addField(type, area) this.selectedAreaRef.value = area diff --git a/app/javascript/template_builder/document.vue b/app/javascript/template_builder/document.vue index ea0b850e..7bba06c7 100644 --- a/app/javascript/template_builder/document.vue +++ b/app/javascript/template_builder/document.vue @@ -23,7 +23,7 @@ @drop-field="$emit('drop-field', {...$event, attachment_uuid: document.uuid })" @remove-area="$emit('remove-area', $event)" @scroll-to="scrollToArea" - @draw="$emit('draw', {...$event, attachment_uuid: document.uuid })" + @draw="$emit('draw', { area: {...$event.area, attachment_uuid: document.uuid }, isTooSmall: $event.isTooSmall })" />
diff --git a/app/javascript/template_builder/page.vue b/app/javascript/template_builder/page.vue index 4ff6bebf..3934f24b 100644 --- a/app/javascript/template_builder/page.vue +++ b/app/javascript/template_builder/page.vue @@ -290,7 +290,12 @@ export default { area.cell_w = this.newArea.cell_w } - this.$emit('draw', area) + const dx = Math.abs(e.offsetX - this.$refs.mask.clientWidth * this.newArea.initialX) + const dy = Math.abs(e.offsetY - this.$refs.mask.clientHeight * this.newArea.initialY) + + const isTooSmall = dx < 8 && dy < 8 + + this.$emit('draw', { area, isTooSmall }) } this.showMask = false From 21138c1d0283b855719014b492d1ec181bece96b Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Thu, 22 May 2025 15:41:19 +0300 Subject: [PATCH 09/15] remove log --- lib/submissions/generate_result_attachments.rb | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/submissions/generate_result_attachments.rb b/lib/submissions/generate_result_attachments.rb index de8e055f..5d697082 100644 --- a/lib/submissions/generate_result_attachments.rb +++ b/lib/submissions/generate_result_attachments.rb @@ -809,12 +809,8 @@ module Submissions data = cache[attachment.uuid] if ICO_REGEXP.match?(attachment.content_type) - Rollbar.error("Load ICO: #{attachment.uuid}") if defined?(Rollbar) - LoadIco.call(data) elsif BMP_REGEXP.match?(attachment.content_type) - Rollbar.error("Load BMP: #{attachment.uuid}") if defined?(Rollbar) - LoadBmp.call(data) else Vips::Image.new_from_buffer(data, '') From 8bc13a5900c7321a6b295db9af31ab2330b80fbd Mon Sep 17 00:00:00 2001 From: Alex Turchyn Date: Fri, 23 May 2025 10:52:47 +0300 Subject: [PATCH 10/15] fix german translations --- config/locales/i18n.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/locales/i18n.yml b/config/locales/i18n.yml index 099dfb02..f66a309d 100644 --- a/config/locales/i18n.yml +++ b/config/locales/i18n.yml @@ -4182,9 +4182,9 @@ de: &de sending: Senden resubmit: Erneut einreichen or: oder - download_documents: Dokumente herunterladen + download_documents: Dokumente downloaden downloading: Lädt herunter - download: Laden + download: Download decline: Ablehnen declined: Abgelehnt decline_reason: Ablehnungsgrund From a3bd37c9a2fe5ad2ff4f78bd9b487f2860aab83f Mon Sep 17 00:00:00 2001 From: Alex Turchyn Date: Sat, 24 May 2025 11:25:49 +0300 Subject: [PATCH 11/15] Improve error message when does not match in completed form --- .../send_submission_email_controller.rb | 21 ++++---- .../submissions_preview_controller.rb | 1 + .../submissions_preview/completed.html.erb | 5 ++ config/locales/i18n.yml | 14 ++++++ spec/system/submission_preview_spec.rb | 50 ++++++++++++++++--- 5 files changed, 74 insertions(+), 17 deletions(-) diff --git a/app/controllers/send_submission_email_controller.rb b/app/controllers/send_submission_email_controller.rb index 06481b1a..6d1f34c3 100644 --- a/app/controllers/send_submission_email_controller.rb +++ b/app/controllers/send_submission_email_controller.rb @@ -10,16 +10,17 @@ class SendSubmissionEmailController < ApplicationController SEND_DURATION = 30.minutes def create - @submitter = - if params[:template_slug] - Submitter.joins(submission: :template).find_by!(email: params[:email].to_s.downcase, - template: { slug: params[:template_slug] }) - elsif params[:submission_slug] - Submitter.joins(:submission).find_by!(email: params[:email].to_s.downcase, - submission: { slug: params[:submission_slug] }) - else - Submitter.find_by!(slug: params[:submitter_slug]) - end + if params[:template_slug] + @submitter = Submitter.joins(submission: :template).find_by!(email: params[:email].to_s.downcase, + template: { slug: params[:template_slug] }) + elsif params[:submission_slug] + @submitter = Submitter.joins(:submission).find_by(email: params[:email].to_s.downcase, + submission: { slug: params[:submission_slug] }) + + return redirect_to submissions_preview_completed_path(params[:submission_slug], status: :error) unless @submitter + else + @submitter = Submitter.find_by!(slug: params[:submitter_slug]) + end RateLimit.call("send-email-#{@submitter.id}", limit: 2, ttl: 5.minutes) diff --git a/app/controllers/submissions_preview_controller.rb b/app/controllers/submissions_preview_controller.rb index 7986e799..d8a36f0b 100644 --- a/app/controllers/submissions_preview_controller.rb +++ b/app/controllers/submissions_preview_controller.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class SubmissionsPreviewController < ApplicationController + around_action :with_browser_locale skip_before_action :authenticate_user! skip_authorization_check diff --git a/app/views/submissions_preview/completed.html.erb b/app/views/submissions_preview/completed.html.erb index bcfea8f9..657eec51 100644 --- a/app/views/submissions_preview/completed.html.erb +++ b/app/views/submissions_preview/completed.html.erb @@ -28,6 +28,11 @@ <%= f.hidden_field :submission_slug, value: @submission.slug %> <%= f.label :email, t('email'), class: 'label' %> <%= f.email_field :email, value: current_user&.email || params[:email], required: true, class: 'base-input', placeholder: t('send_copy_to_email') %> + <% if params[:status] == 'error' %> + + <%= t('please_enter_your_email_address_associated_with_the_completed_submission') %> + + <% end %>
<%= f.button button_title(title: t('send_copy_to_email'), disabled_with: t('starting')), class: 'base-button' %> diff --git a/config/locales/i18n.yml b/config/locales/i18n.yml index f66a309d..d807d374 100644 --- a/config/locales/i18n.yml +++ b/config/locales/i18n.yml @@ -744,6 +744,7 @@ en: &en two_months: 2 months three_months: 3 months eu_data_residency: EU data residency + please_enter_your_email_address_associated_with_the_completed_submission: Please enter your email address associated with the completed submission. submission_sources: api: API bulk: Bulk Send @@ -1555,6 +1556,7 @@ es: &es two_months: 2 meses three_months: 3 meses eu_data_residency: Datos alojados UE + please_enter_your_email_address_associated_with_the_completed_submission: Por favor, introduce tu dirección de correo electrónico asociada con el envío completado. submission_sources: api: API bulk: Envío masivo @@ -2364,6 +2366,7 @@ it: &it two_months: 2 mesi three_months: 3 mesi eu_data_residency: "Dati nell'UE" + please_enter_your_email_address_associated_with_the_completed_submission: "Inserisci il tuo indirizzo email associato all'invio completato." submission_sources: api: API bulk: Invio massivo @@ -3176,6 +3179,7 @@ fr: &fr two_months: 2 mois three_months: 3 mois eu_data_residency: "Données dans l'UE" + please_enter_your_email_address_associated_with_the_completed_submission: "Veuillez saisir l'adresse e-mail associée à l'envoi complété." submission_sources: api: API bulk: Envoi en masse @@ -3987,6 +3991,7 @@ pt: &pt two_months: 2 meses three_months: 3 meses eu_data_residency: Dados na UE + please_enter_your_email_address_associated_with_the_completed_submission: Por favor, insira seu e-mail associado ao envio concluído. submission_sources: api: API bulk: Envio em massa @@ -4799,6 +4804,7 @@ de: &de two_months: 2 Monate three_months: 3 Monate eu_data_residency: EU-Datenspeicher + please_enter_your_email_address_associated_with_the_completed_submission: Bitte gib deine E-Mail-Adresse ein, die mit der abgeschlossenen Übermittlung verknüpft ist. submission_sources: api: API bulk: Massenversand @@ -4951,6 +4957,7 @@ pl: count_documents_signed_with_html: '%{count} dokumentów podpisanych za pomocą' open_source_documents_software: 'oprogramowanie do dokumentów open source' eu_data_residency: Dane w UE + please_enter_your_email_address_associated_with_the_completed_submission: Wprowadź adres e-mail powiązany z ukończonym zgłoszeniem. uk: require_phone_2fa_to_open: Вимагати двофакторну автентифікацію через телефон для відкриття @@ -5019,6 +5026,7 @@ uk: count_documents_signed_with_html: '%{count} документів підписано за допомогою' open_source_documents_software: 'відкрите програмне забезпечення для документів' eu_data_residency: 'Зберігання даних в ЄС' + please_enter_your_email_address_associated_with_the_completed_submission: "Введіть адресу електронної пошти, пов'язану із завершеним поданням." cs: require_phone_2fa_to_open: Vyžadovat otevření pomocí telefonního 2FA @@ -5087,6 +5095,7 @@ cs: count_documents_signed_with_html: '%{count} dokumentů podepsáno pomocí' open_source_documents_software: 'open source software pro dokumenty' eu_data_residency: 'Uložení dat v EU' + please_enter_your_email_address_associated_with_the_completed_submission: Zadejte e-mailovou adresu spojenou s dokončeným odesláním. he: require_phone_2fa_to_open: דרוש אימות דו-שלבי באמצעות טלפון לפתיחה @@ -5155,6 +5164,7 @@ he: count_documents_signed_with_html: '%{count} מסמכים נחתמו באמצעות' open_source_documents_software: 'תוכנה בקוד פתוח למסמכים' eu_data_residency: 'נתונים באיחוד האירופי ' + please_enter_your_email_address_associated_with_the_completed_submission: 'אנא הזן את כתובת הדוא"ל המשויכת למשלוח שהושלם.' nl: require_phone_2fa_to_open: Vereis telefoon 2FA om te openen @@ -5223,6 +5233,7 @@ nl: count_documents_signed_with_html: '%{count} documenten ondertekend met' open_source_documents_software: 'open source documenten software' eu_data_residency: Gegevens EU + please_enter_your_email_address_associated_with_the_completed_submission: Voer het e-mailadres in dat is gekoppeld aan de voltooide indiening. ar: require_phone_2fa_to_open: "تطلب فتح عبر تحقق الهاتف ذو العاملين" @@ -5291,6 +5302,7 @@ ar: count_documents_signed_with_html: '%{count} مستندات تم توقيعها باستخدام' open_source_documents_software: 'برنامج مستندات مفتوح المصدر' eu_data_residency: 'بيانات في الاتحاد الأوروبي' + please_enter_your_email_address_associated_with_the_completed_submission: 'يرجى إدخال عنوان البريد الإلكتروني المرتبط بالإرسال المكتمل.' ko: require_phone_2fa_to_open: 휴대폰 2FA를 열 때 요구함 @@ -5359,6 +5371,7 @@ ko: count_documents_signed_with_html: '%{count}개의 문서가 다음을 통해 서명됨' open_source_documents_software: '오픈소스 문서 소프트웨어' eu_data_residency: 'EU 데이터 보관' + please_enter_your_email_address_associated_with_the_completed_submission: '완료된 제출과 연결된 이메일 주소를 입력하세요.' ja: require_phone_2fa_to_open: 電話による2段階認証が必要です @@ -5427,6 +5440,7 @@ ja: count_documents_signed_with_html: '%{count} 件のドキュメントが以下で署名されました' open_source_documents_software: 'オープンソースのドキュメントソフトウェア' eu_data_residency: 'EU データ居住' + please_enter_your_email_address_associated_with_the_completed_submission: '完了した提出に関連付けられたメールアドレスを入力してください。' en-US: <<: *en diff --git a/spec/system/submission_preview_spec.rb b/spec/system/submission_preview_spec.rb index 54fb75f2..3048e96d 100644 --- a/spec/system/submission_preview_spec.rb +++ b/spec/system/submission_preview_spec.rb @@ -6,18 +6,54 @@ RSpec.describe 'Submission Preview' do let(:template) { create(:template, account:, author: user) } context 'when not submitted' do - let(:submission) { create(:submission, template:, created_by_user: user) } + let(:submission) { create(:submission, :with_submitters, template:, created_by_user: user) } - before do - template.submitters.map { |s| create(:submitter, submission:, uuid: s['uuid']) } + context 'when user is signed in' do + before do + sign_in(user) - sign_in(user) + visit submissions_preview_path(slug: submission.slug) + end - visit submissions_preview_path(slug: submission.slug) + it 'completes the form' do + expect(page).to have_content('Not completed') + end end - it 'completes the form' do - expect(page).to have_content('Not completed') + context 'when user is not signed in' do + context 'when submission is not completed' do + before do + create(:encrypted_config, account:, key: EncryptedConfig::EMAIL_SMTP_KEY, value: '{}') + + submission.submitters.each { |s| s.update(completed_at: 1.day.ago) } + + visit submissions_preview_path(slug: submission.slug) + end + + it "sends a copy to the submitter's email" do + fill_in 'Email', with: submission.submitters.first.email + click_button 'Send copy to Email' + + expect(page).to have_content('Email has been sent.') + end + + it 'shows an error for an email not associated with the submission' do + fill_in 'Email', with: 'john.due@example.com' + click_button 'Send copy to Email' + + expect(page).to have_content('Please enter your email address associated with the completed submission.') + end + end + + it "doesn't display the email form if SMTP is not configured" do + submission.submitters.each { |s| s.update(completed_at: 1.day.ago) } + + visit submissions_preview_path(slug: submission.slug) + + expect(page).to have_content(template.name) + expect(page).not_to have_field('Email') + expect(page).not_to have_content('Send copy to Email') + end end end end From 343118d00d7f11de0533d72b647721cc6130d6ef Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Sat, 24 May 2025 11:54:00 +0300 Subject: [PATCH 12/15] fix bmp --- lib/load_bmp.rb | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/lib/load_bmp.rb b/lib/load_bmp.rb index b5ea074a..2dc3269e 100644 --- a/lib/load_bmp.rb +++ b/lib/load_bmp.rb @@ -36,16 +36,12 @@ module LoadBmp image_rgb = if bands == 3 - Vips::Image.bandjoin([image[2], image[1], image[0]]) + image.recomb(band3_recomb) elsif bands == 4 - Vips::Image.bandjoin([image[2], image[1], image[0], image[3]]) - else - image + image.recomb(band4_recomb) end - if image_rgb.interpretation == :multiband || image_rgb.interpretation == :'b-w' - image_rgb = image_rgb.copy(interpretation: :srgb) - end + image_rgb = image_rgb.copy(interpretation: :srgb) if image_rgb.interpretation != :srgb image_rgb end @@ -166,5 +162,26 @@ module LoadBmp unpadded_rows.join end + + def band3_recomb + @band3_recomb ||= + Vips::Image.new_from_array( + [ + [0, 0, 1], + [0, 1, 0], + [1, 0, 0] + ] + ) + end + + def band4_recomb + @band4_recomb ||= Vips::Image.new_from_array( + [ + [0, 0, 1, 0], + [0, 1, 0, 0], + [1, 0, 0, 0] + ] + ) + end # rubocop:enable Metrics end From f78cf8cc6e2849634f2b56e2b8b34630948a7413 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Sat, 24 May 2025 15:05:24 +0300 Subject: [PATCH 13/15] pdfium --- .github/workflows/ci.yml | 5 +- Dockerfile | 21 +- lib/pdfium.rb | 412 ++++++++++++++++++++++++++++++ lib/templates/process_document.rb | 74 +++--- 4 files changed, 471 insertions(+), 41 deletions(-) create mode 100644 lib/pdfium.rb diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c4d56d7b..eb307922 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -157,7 +157,10 @@ jobs: bundle install --jobs 4 --retry 4 yarn install sudo apt-get update - sudo apt-get install libvips + sudo apt-get install -y libvips + wget -O pdfium-linux.tgz "https://github.com/docusealco/pdfium-binaries/releases/latest/download/pdfium-linux-$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/').tgz" + sudo tar -xzf pdfium-linux.tgz --strip-components=1 -C /usr/lib lib/libpdfium.so + rm -f pdfium-linux.tgz - name: Run env: RAILS_ENV: test diff --git a/Dockerfile b/Dockerfile index 99753c93..932d8b13 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,17 @@ -FROM ruby:3.4.2-alpine AS fonts +FROM ruby:3.4.2-alpine AS download WORKDIR /fonts -RUN apk --no-cache add fontforge wget && wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Regular.ttf && wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Bold.ttf && wget https://github.com/impallari/DancingScript/raw/master/fonts/DancingScript-Regular.otf && wget https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSansSymbols2/hinted/ttf/NotoSansSymbols2-Regular.ttf && wget https://github.com/Maxattax97/gnu-freefont/raw/master/ttf/FreeSans.ttf && wget https://github.com/impallari/DancingScript/raw/master/OFL.txt +RUN apk --no-cache add fontforge wget && \ + wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Regular.ttf && \ + wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Bold.ttf && \ + wget https://github.com/impallari/DancingScript/raw/master/fonts/DancingScript-Regular.otf && \ + wget https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSansSymbols2/hinted/ttf/NotoSansSymbols2-Regular.ttf && \ + wget https://github.com/Maxattax97/gnu-freefont/raw/master/ttf/FreeSans.ttf && \ + wget https://github.com/impallari/DancingScript/raw/master/OFL.txt && \ + wget -O pdfium-linux.tgz "https://github.com/docusealco/pdfium-binaries/releases/latest/download/pdfium-linux-$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/').tgz" && \ + mkdir -p /pdfium-linux && \ + tar -xzf pdfium-linux.tgz -C /pdfium-linux RUN fontforge -lang=py -c 'font1 = fontforge.open("FreeSans.ttf"); font2 = fontforge.open("NotoSansSymbols2-Regular.ttf"); font1.mergeFonts(font2); font1.generate("FreeSans.ttf")' @@ -41,7 +50,7 @@ ENV OPENSSL_CONF=/app/openssl_legacy.cnf WORKDIR /app -RUN echo '@edge https://dl-cdn.alpinelinux.org/alpine/edge/community' >> /etc/apk/repositories && apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler poppler-utils redis libheif@edge vips-heif gcompat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf +RUN echo '@edge https://dl-cdn.alpinelinux.org/alpine/edge/community' >> /etc/apk/repositories && apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev@edge redis libheif@edge vips-heif gcompat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf RUN echo $'.include = /etc/ssl/openssl.cnf\n\ \n\ @@ -70,8 +79,10 @@ COPY ./tmp ./tmp COPY LICENSE README.md Rakefile config.ru .version ./ COPY .version ./public/version -COPY --from=fonts /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts -COPY --from=fonts /fonts/FreeSans.ttf /usr/share/fonts/freefont +COPY --from=download /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts +COPY --from=download /fonts/FreeSans.ttf /usr/share/fonts/freefont +COPY --from=download /pdfium-linux/lib/libpdfium.so /usr/lib/libpdfium.so +COPY --from=download /pdfium-linux/licenses/pdfium.txt /usr/lib/libpdfium-LICENSE.txt COPY --from=webpack /app/public/packs ./public/packs RUN ln -s /fonts /app/public/fonts diff --git a/lib/pdfium.rb b/lib/pdfium.rb new file mode 100644 index 00000000..554f7a6b --- /dev/null +++ b/lib/pdfium.rb @@ -0,0 +1,412 @@ +# frozen_string_literal: true + +class Pdfium + extend FFI::Library + + LIB_NAME = 'pdfium' + + begin + ffi_lib case FFI::Platform::OS + when 'darwin' + [ + "lib#{LIB_NAME}.dylib", + '/Applications/LibreOffice.app/Contents/Frameworks/libpdfiumlo.dylib' + ] + else + "lib#{LIB_NAME}.so" + end + rescue LoadError => e + raise "Could not load libpdfium library. Make sure it's installed and in your library path. Error: #{e.message}" + end + + typedef :pointer, :FPDF_STRING + typedef :pointer, :FPDF_DOCUMENT + typedef :pointer, :FPDF_PAGE + typedef :pointer, :FPDF_BITMAP + typedef :pointer, :FPDF_FORMHANDLE + + MAX_SIZE = 32_767 + + FPDF_ANNOT = 0x01 + FPDF_LCD_TEXT = 0x02 + FPDF_NO_NATIVETEXT = 0x04 + FPDF_GRAYSCALE = 0x08 + FPDF_REVERSE_BYTE_ORDER = 0x10 + FPDF_RENDER_LIMITEDIMAGECACHE = 0x200 + FPDF_RENDER_FORCEHALFTONE = 0x400 + FPDF_PRINTING = 0x800 + + # rubocop:disable Naming/ClassAndModuleCamelCase + class FPDF_LIBRARY_CONFIG < FFI::Struct + layout :version, :int, + :m_pUserFontPaths, :pointer, + :m_pIsolate, :pointer, + :m_v8EmbedderSlot, :uint, + :m_pPlatform, :pointer, + :m_RendererType, :int + end + # rubocop:enable Naming/ClassAndModuleCamelCase + + attach_function :FPDF_InitLibraryWithConfig, [:pointer], :void + attach_function :FPDF_DestroyLibrary, [], :void + + attach_function :FPDF_LoadDocument, %i[string FPDF_STRING], :FPDF_DOCUMENT + attach_function :FPDF_LoadMemDocument, %i[pointer int FPDF_STRING], :FPDF_DOCUMENT + attach_function :FPDF_CloseDocument, [:FPDF_DOCUMENT], :void + attach_function :FPDF_GetPageCount, [:FPDF_DOCUMENT], :int + attach_function :FPDF_GetLastError, [], :ulong + + attach_function :FPDF_LoadPage, %i[FPDF_DOCUMENT int], :FPDF_PAGE + attach_function :FPDF_ClosePage, [:FPDF_PAGE], :void + attach_function :FPDF_GetPageWidthF, [:FPDF_PAGE], :float + attach_function :FPDF_GetPageHeightF, [:FPDF_PAGE], :float + + attach_function :FPDFBitmap_Create, %i[int int int], :FPDF_BITMAP + attach_function :FPDFBitmap_CreateEx, %i[int int int pointer int], :FPDF_BITMAP + attach_function :FPDFBitmap_Destroy, [:FPDF_BITMAP], :void + attach_function :FPDFBitmap_GetBuffer, [:FPDF_BITMAP], :pointer + attach_function :FPDFBitmap_GetWidth, [:FPDF_BITMAP], :int + attach_function :FPDFBitmap_GetHeight, [:FPDF_BITMAP], :int + attach_function :FPDFBitmap_GetStride, [:FPDF_BITMAP], :int + attach_function :FPDFBitmap_FillRect, %i[FPDF_BITMAP int int int int ulong], :void + + attach_function :FPDF_RenderPageBitmap, %i[FPDF_BITMAP FPDF_PAGE int int int int int int], :void + + typedef :int, :FPDF_BOOL + typedef :pointer, :IPDF_JSPLATFORM + + # rubocop:disable Naming/ClassAndModuleCamelCase + class FPDF_FORMFILLINFO_V2 < FFI::Struct + layout :version, :int, + :Release, :pointer, + :FFI_Invalidate, :pointer, + :FFI_OutputSelectedRect, :pointer, + :FFI_SetCursor, :pointer, + :FFI_SetTimer, :pointer, + :FFI_KillTimer, :pointer, + :FFI_GetLocalTime, :pointer, + :FFI_OnChange, :pointer, + :FFI_GetPage, :pointer, + :FFI_GetCurrentPage, :pointer, + :FFI_GetRotation, :pointer, + :FFI_ExecuteNamedAction, :pointer, + :FFI_SetTextFieldFocus, :pointer, + :FFI_DoURIAction, :pointer, + :FFI_DoGoToAction, :pointer, + :m_pJsPlatform, :IPDF_JSPLATFORM, + :xfa_disabled, :FPDF_BOOL, + :FFI_DisplayCaret, :pointer, + :FFI_GetCurrentPageIndex, :pointer, + :FFI_SetCurrentPage, :pointer, + :FFI_GotoURL, :pointer, + :FFI_GetPageViewRect, :pointer, + :FFI_PageEvent, :pointer, + :FFI_PopupMenu, :pointer, + :FFI_OpenFile, :pointer, + :FFI_EmailTo, :pointer, + :FFI_UploadTo, :pointer, + :FFI_GetPlatform, :pointer, + :FFI_GetLanguage, :pointer, + :FFI_DownloadFromURL, :pointer, + :FFI_PostRequestURL, :pointer, + :FFI_PutRequestURL, :pointer, + :FFI_OnFocusChange, :pointer, + :FFI_DoURIActionWithKeyboardModifier, :pointer + end + # rubocop:enable Naming/ClassAndModuleCamelCase + + attach_function :FPDFDOC_InitFormFillEnvironment, %i[FPDF_DOCUMENT pointer], :FPDF_FORMHANDLE + attach_function :FPDFDOC_ExitFormFillEnvironment, [:FPDF_FORMHANDLE], :void + attach_function :FPDF_FFLDraw, %i[FPDF_FORMHANDLE FPDF_BITMAP FPDF_PAGE int int int int int int], :void + + FPDF_ERR_SUCCESS = 0 + FPDF_ERR_UNKNOWN = 1 + FPDF_ERR_FILE = 2 + FPDF_ERR_FORMAT = 3 + FPDF_ERR_PASSWORD = 4 + FPDF_ERR_SECURITY = 5 + FPDF_ERR_PAGE = 6 + + PDFIUM_ERRORS = { + FPDF_ERR_SUCCESS => 'Success', + FPDF_ERR_UNKNOWN => 'Unknown error', + FPDF_ERR_FILE => 'Error open file', + FPDF_ERR_FORMAT => 'Invalid format', + FPDF_ERR_PASSWORD => 'Incorrect password', + FPDF_ERR_SECURITY => 'Security scheme error', + FPDF_ERR_PAGE => 'Page not found' + }.freeze + + class PdfiumError < StandardError; end + + def self.error_message(code) + PDFIUM_ERRORS[code] || "Unknown error code: #{code}" + end + + def self.check_last_error(context_message = 'PDFium operation failed') + error_code = FPDF_GetLastError() + + return if error_code == FPDF_ERR_SUCCESS + + raise PdfiumError, "#{context_message}: #{error_message(error_code)} (Code: #{error_code})" + end + + class Document + attr_reader :document_ptr, :form_handle + + def initialize(document_ptr, source_buffer = nil) + raise ArgumentError, 'document_ptr cannot be nil' if document_ptr.nil? || document_ptr.null? + + @document_ptr = document_ptr + + @pages = {} + @closed = false + @source_buffer = source_buffer + @form_handle = FFI::Pointer::NULL + @form_fill_info_mem = FFI::Pointer::NULL + + init_form_fill_environment + end + + def init_form_fill_environment + return if @document_ptr.null? + + @form_fill_info_mem = FFI::MemoryPointer.new(FPDF_FORMFILLINFO_V2.size) + + form_fill_info_struct = FPDF_FORMFILLINFO_V2.new(@form_fill_info_mem) + form_fill_info_struct[:version] = 2 + + @form_handle = Pdfium.FPDFDOC_InitFormFillEnvironment(@document_ptr, @form_fill_info_mem) + end + + def page_count + @page_count ||= Pdfium.FPDF_GetPageCount(@document_ptr) + end + + def self.open_file(file_path, password = nil) + doc_ptr = Pdfium.FPDF_LoadDocument(file_path, password) + + if doc_ptr.null? + Pdfium.check_last_error("Failed to load document from file '#{file_path}'") + + raise PdfiumError, "Failed to load document from file '#{file_path}', pointer is NULL." + end + + doc = new(doc_ptr) + + return doc unless block_given? + + begin + yield doc + ensure + doc.close + end + end + + def self.open_bytes(bytes, password = nil) + buffer = FFI::MemoryPointer.new(:char, bytes.bytesize) + buffer.put_bytes(0, bytes) + + doc_ptr = Pdfium.FPDF_LoadMemDocument(buffer, bytes.bytesize, password) + + if doc_ptr.null? + Pdfium.check_last_error('Failed to load document from memory') + + raise PdfiumError, 'Failed to load document from memory, pointer is NULL.' + end + + doc = new(doc_ptr, buffer) + + return doc unless block_given? + + begin + yield doc + ensure + doc.close + end + end + + def closed? + @closed + end + + def ensure_not_closed! + raise PdfiumError, 'Document is closed.' if closed? + end + + def get_page(page_index) + ensure_not_closed! + + unless page_index.is_a?(Integer) && page_index >= 0 && page_index < page_count + raise PdfiumError, "Page index #{page_index} out of range (0..#{page_count - 1})" + end + + @pages[page_index] ||= Page.new(self, page_index) + end + + def close + return if closed? + + @pages.each_value { |page| page.close unless page.closed? } + @pages.clear + + unless @form_handle.null? + Pdfium.FPDFDOC_ExitFormFillEnvironment(@form_handle) + + @form_handle = FFI::Pointer::NULL + end + + if @form_fill_info_mem && !@form_fill_info_mem.null? + @form_fill_info_mem.free + @form_fill_info_mem = FFI::Pointer::NULL + end + + Pdfium.FPDF_CloseDocument(@document_ptr) unless @document_ptr.null? + + @document_ptr = FFI::Pointer::NULL + @source_buffer = nil + + @closed = true + end + end + + class Page + attr_reader :document, :page_index, :page_ptr + + def initialize(document, page_index) + raise ArgumentError, 'Document object is required' unless document.is_a?(Pdfium::Document) + + @document = document + @document.ensure_not_closed! + + @page_index = page_index + + @page_ptr = Pdfium.FPDF_LoadPage(document.document_ptr, page_index) + + if @page_ptr.null? + Pdfium.check_last_error("Failed to load page #{page_index}") + + raise PdfiumError, "Failed to load page #{page_index}, pointer is NULL." + end + + @closed = false + end + + def width + @width ||= Pdfium.FPDF_GetPageWidthF(@page_ptr) + end + + def height + @height ||= Pdfium.FPDF_GetPageHeightF(@page_ptr) + end + + def closed? + @closed + end + + def form_handle + @document.form_handle + end + + def ensure_not_closed! + raise PdfiumError, 'Page is closed.' if closed? + + @document.ensure_not_closed! + end + + def render_to_bitmap(width: nil, height: nil, scale: nil, background_color: 0xFFFFFFFF, + flags: FPDF_ANNOT | FPDF_LCD_TEXT | FPDF_NO_NATIVETEXT | FPDF_REVERSE_BYTE_ORDER) + ensure_not_closed! + + render_width, render_height = calculate_render_dimensions(width, height, scale) + + bitmap_ptr = Pdfium.FPDFBitmap_Create(render_width, render_height, 1) + + if bitmap_ptr.null? + Pdfium.check_last_error('Failed to create bitmap (potential pre-existing error)') + + raise PdfiumError, 'Failed to create bitmap (FPDFBitmap_Create returned NULL)' + end + + Pdfium.FPDFBitmap_FillRect(bitmap_ptr, 0, 0, render_width, render_height, background_color) + + Pdfium.FPDF_RenderPageBitmap(bitmap_ptr, page_ptr, 0, 0, render_width, render_height, 0, flags) + + Pdfium.check_last_error('Failed to render page to bitmap') + + unless form_handle.null? + Pdfium.FPDF_FFLDraw(form_handle, bitmap_ptr, page_ptr, 0, 0, render_width, render_height, 0, flags) + + Pdfium.check_last_error('Call to FPDF_FFLDraw completed (check for rendering issues if any)') + end + + buffer_ptr = Pdfium.FPDFBitmap_GetBuffer(bitmap_ptr) + stride = Pdfium.FPDFBitmap_GetStride(bitmap_ptr) + + bitmap_data = buffer_ptr.read_bytes(stride * render_height) + + [bitmap_data, render_width, render_height] + ensure + Pdfium.FPDFBitmap_Destroy(bitmap_ptr) if bitmap_ptr && !bitmap_ptr.null? + end + + def close + return if closed? + + Pdfium.FPDF_ClosePage(@page_ptr) unless @page_ptr.null? + + @page_ptr = FFI::Pointer::NULL + + @closed = true + end + + private + + def calculate_render_dimensions(width_param, height_param, scale_param) + if scale_param + render_width = (width * scale_param).round + render_height = (height * scale_param).round + elsif width_param || height_param + if width_param && height_param + render_width = width_param + render_height = height_param + elsif width_param + scale_factor = width_param.to_f / width + render_width = width_param + render_height = (height * scale_factor).round + else + scale_factor = height_param.to_f / height + render_width = (width * scale_factor).round + render_height = height_param + end + else + render_width = width.to_i + render_height = height.to_i + end + + [render_width.clamp(1, MAX_SIZE), render_height.clamp(1, MAX_SIZE)] + end + end + + def self.initialize_library + config_mem = FFI::MemoryPointer.new(FPDF_LIBRARY_CONFIG.size) + + config_struct = FPDF_LIBRARY_CONFIG.new(config_mem) + config_struct[:version] = 2 + config_struct[:m_pUserFontPaths] = FFI::Pointer::NULL + config_struct[:m_pIsolate] = FFI::Pointer::NULL + config_struct[:m_v8EmbedderSlot] = 0 + + FPDF_InitLibraryWithConfig(config_mem) + end + + def self.cleanup_library + FPDF_DestroyLibrary() + end + + initialize_library + + at_exit do + cleanup_library + end +end diff --git a/lib/templates/process_document.rb b/lib/templates/process_document.rb index 54174b06..8ee1dff5 100644 --- a/lib/templates/process_document.rb +++ b/lib/templates/process_document.rb @@ -6,6 +6,7 @@ module Templates FORMAT = '.png' ATTACHMENT_NAME = 'preview_images' + BMP_REGEXP = %r{\Aimage/(?:bmp|x-bmp|x-ms-bmp)\z} PDF_CONTENT_TYPE = 'application/pdf' CONCURRENCY = 2 Q = 95 @@ -38,7 +39,13 @@ module Templates def generate_preview_image(attachment, data) ActiveStorage::Attachment.where(name: ATTACHMENT_NAME, record: attachment).destroy_all - image = Vips::Image.new_from_buffer(data, '') + image = + if BMP_REGEXP.match?(attachment.content_type) + LoadBmp.call(data) + else + Vips::Image.new_from_buffer(data, '') + end + image = image.autorot.resize(MAX_WIDTH / image.width.to_f) bitdepth = 2**image.stats.to_a[1..3].pluck(2).uniq.size @@ -77,11 +84,13 @@ module Templates end def generate_document_preview_images(attachment, data, range, concurrency: CONCURRENCY) + doc = Pdfium::Document.open_bytes(data) + pool = Concurrent::FixedThreadPool.new(concurrency) promises = range.map do |page_number| - Concurrent::Promise.execute(executor: pool) { build_and_upload_blob(data, page_number) } + Concurrent::Promise.execute(executor: pool) { build_and_upload_blob(doc, page_number) } end Concurrent::Promise.zip(*promises).value!.each do |blob| @@ -95,31 +104,42 @@ module Templates ) end end - - pool.kill + ensure + doc&.close + pool&.kill end - def build_and_upload_blob(data, page_number) - page = Vips::Image.new_from_buffer(data, '', dpi: DPI, page: page_number) - page = page.resize(MAX_WIDTH / page.width.to_f) + def build_and_upload_blob(doc, page_number, format = FORMAT) + doc_page = doc.get_page(page_number) + + data, width, height = doc_page.render_to_bitmap(width: MAX_WIDTH) + + page = Vips::Image.new_from_memory(data, width, height, 4, :uchar) + + page = page.copy(interpretation: :srgb) bitdepth = 2**page.stats.to_a[1..3].pluck(2).uniq.size - io = StringIO.new(page.write_to_buffer(FORMAT, compression: 7, filter: 0, bitdepth:, - palette: true, Q: bitdepth == 8 ? Q : 5, dither: 0)) + data = + if format == FORMAT + page.write_to_buffer(format, compression: 7, filter: 0, bitdepth:, + palette: true, Q: bitdepth == 8 ? Q : 5, dither: 0) + else + page.write_to_buffer(format, interlace: true, Q: JPEG_Q) + end blob = ActiveStorage::Blob.new( - filename: "#{page_number}#{FORMAT}", + filename: "#{page_number}#{format}", metadata: { analyzed: true, identified: true, width: page.width, height: page.height } ) - blob.upload(io) + blob.upload(StringIO.new(data)) blob - rescue Vips::Error => e + rescue Vips::Error, Pdfium::PdfiumError => e Rollbar.warning(e) if defined?(Rollbar) - - nil + ensure + doc_page&.close end def maybe_flatten_form(data, pdf) @@ -162,35 +182,19 @@ module Templates end def generate_pdf_preview_from_file(attachment, file_path, page_number) - io = StringIO.new - - command = [ - 'pdftocairo', '-jpeg', '-jpegopt', "progressive=y,quality=#{JPEG_Q},optimize=y", - '-scale-to-x', MAX_WIDTH, '-scale-to-y', '-1', - '-r', DPI, '-f', page_number + 1, '-l', page_number + 1, - '-singlefile', Shellwords.escape(file_path), '-' - ].join(' ') - - Open3.popen3(command) do |_, stdout, _, _| - io.write(stdout.read) - - io.rewind - end - - page = Vips::Image.new_from_buffer(io.read, '') + doc = Pdfium::Document.open_file(file_path) - io.rewind + blob = build_and_upload_blob(doc, page_number, '.jpeg') ApplicationRecord.no_touching do ActiveStorage::Attachment.create!( - blob: ActiveStorage::Blob.create_and_upload!( - io:, filename: "#{page_number}.jpg", - metadata: { analyzed: true, identified: true, width: page.width, height: page.height } - ), + blob: blob, name: ATTACHMENT_NAME, record: attachment ) end + ensure + doc&.close end end end From 16903376998e71ee2f9787412b3feca077d682d9 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Mon, 26 May 2025 10:11:01 +0300 Subject: [PATCH 14/15] fix pdf image result --- lib/submissions/generate_result_attachments.rb | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/submissions/generate_result_attachments.rb b/lib/submissions/generate_result_attachments.rb index 5d697082..e260192d 100644 --- a/lib/submissions/generate_result_attachments.rb +++ b/lib/submissions/generate_result_attachments.rb @@ -735,14 +735,16 @@ module Submissions page = pdf.pages.add - scale = [A4_SIZE.first / attachment.metadata['width'].to_f, - A4_SIZE.last / attachment.metadata['height'].to_f].min + image = attachment.preview_images.first - page.box.width = attachment.metadata['width'] * scale - page.box.height = attachment.metadata['height'] * scale + scale = [A4_SIZE.first / image.metadata['width'].to_f, + A4_SIZE.last / image.metadata['height'].to_f].min + + page.box.width = image.metadata['width'] * scale + page.box.height = image.metadata['height'] * scale page.canvas.image( - StringIO.new(attachment.preview_images.first.download), + StringIO.new(image.download), at: [0, 0], width: page.box.width, height: page.box.height From 08aa902e07afaeb0cd1769eb94a2657d52942cbe Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Mon, 26 May 2025 11:58:47 +0300 Subject: [PATCH 15/15] set from user id --- app/mailers/application_mailer.rb | 10 +++++++--- app/mailers/submitter_mailer.rb | 10 +++++++++- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb index 4f2cca3d..945994ed 100644 --- a/app/mailers/application_mailer.rb +++ b/app/mailers/application_mailer.rb @@ -20,7 +20,7 @@ class ApplicationMailer < ActionMailer::Base end def set_message_metadata - message.instance_variable_set(:@message_metadata, @message_metadata) + message.instance_variable_set(:@message_metadata, @message_metadata || {}) end def set_message_uuid @@ -28,10 +28,14 @@ class ApplicationMailer < ActionMailer::Base end def assign_message_metadata(tag, record) - @message_metadata = { + @message_metadata = (@message_metadata || {}).merge( 'tag' => tag, 'record_id' => record.id, 'record_type' => record.class.name - } + ) + end + + def put_metadata(attrs) + @message_metadata = (@message_metadata || {}).merge(attrs) end end diff --git a/app/mailers/submitter_mailer.rb b/app/mailers/submitter_mailer.rb index acddfdbb..769995ed 100644 --- a/app/mailers/submitter_mailer.rb +++ b/app/mailers/submitter_mailer.rb @@ -233,9 +233,17 @@ class SubmitterMailer < ApplicationMailer def from_address_for_submitter(submitter) if submitter.submission.source.in?(%w[api embed]) && (from_email = AccountConfig.find_by(account: submitter.account, key: 'integration_from_email')&.value.presence) + user = submitter.account.users.find_by(email: from_email) + + put_metadata('from_user_id' => user.id) + from_email else - (submitter.submission.created_by_user || submitter.submission.template.author).friendly_name + user = submitter.submission.created_by_user || submitter.submission.template.author + + put_metadata('from_user_id' => user.id) + + user.friendly_name end end end