# frozen_string_literal: true module Templates module FindAcroFields PDF_CONTENT_TYPE = 'application/pdf' FIELD_NAME_REGEXP = /\A(?=.*\p{L})[\p{L}\d\s-]+\z/ SKIP_FIELD_DESCRIPTION = %w[undefined].freeze SELECT_PLACEHOLDER_REGEXP = /\b( Select | Choose | Wählen | Auswählen | Sélectionner| Choisir | Seleccionar | Elegir | Seleziona | Scegliere | Selecionar | Escolher )\b/ix DATE_FORMAT_REGEXP = %r{[myd]{2,4}[-\\/\s.][myd]{2,4}[-\\/\s.][myd]{2,4}}i FIELD_ALIGNMENT = { 0 => 'left', 1 => 'center', 2 => 'right' }.freeze module_function # rubocop:disable Metrics def call(pdf, attachment, data) return [] if pdf.acro_form.blank? && data.exclude?('/Form') fields, annots_index = build_fields_with_pages(pdf) fields.filter_map do |field| areas = Array.wrap(field[:Kids] || field).filter_map do |child_field| page = annots_index[child_field.hash] next unless page media_box = page[:CropBox] || page[:MediaBox] crop_box = page[:CropBox] || media_box media_box_start = [media_box[0], media_box[1]] crop_shift = [crop_box[0] - media_box[0], crop_box[1] - media_box[1]] next unless child_field[:Rect] x0, y0, x1, y1 = child_field[:Rect] x0, y0 = correct_coordinates(x0, y0, crop_shift, media_box_start) x1, y1 = correct_coordinates(x1, y1, crop_shift, media_box_start) page_width = media_box[2] - media_box[0] page_height = media_box[3] - media_box[1] x = x0 y = y0 w = x1 - x0 h = y1 - y0 transformed_y = page_height - y - h attrs = { page: page.index, x: x / page_width.to_f, y: transformed_y / page_height.to_f, w: w / page_width.to_f, h: h / page_height.to_f, attachment_uuid: attachment.uuid } next if attrs[:w].zero? || attrs[:h].zero? if child_field[:MaxLen] && child_field.try(:concrete_field_type) == :comb_text_field attrs[:cell_w] = w / page_width / child_field[:MaxLen].to_f end attrs end next if areas.blank? field_properties = build_field_properties(field) next if field_properties.blank? next if field_properties[:default_value].present? if field_properties[:type].in?(%w[radio multiple]) if areas.size != field_properties[:options].size field_properties[:options] = build_options(Array.new(areas.size, '')) end areas.each_with_index do |area, index| area[:option_uuid] = field_properties[:options][index][:uuid] end end { uuid: SecureRandom.uuid, required: field.flags.include?(:required), preferences: {}, areas:, **field_properties } end rescue StandardError => e raise if Rails.env.local? Rollbar.error(e) if defined?(Rollbar) [] end def correct_coordinates(x_coord, y_coord, shift, media_box_start) corrected_x = x_coord + shift[0] - media_box_start[0] corrected_y = y_coord + shift[1] - media_box_start[1] [corrected_x, corrected_y] end def build_field_properties(field) field_name = field.full_field_name if field.full_field_name.to_s.match?(FIELD_NAME_REGEXP) field_name = field_name&.encode('utf-8', invalid: :replace, undef: :replace, replace: '') attrs = { name: field_name.to_s } attrs[:description] = field[:TU] if field[:TU].present? && field[:TU] != field.full_field_name && !field[:TU].in?(SKIP_FIELD_DESCRIPTION) if field[:Q].present? && field.field_type == :Tx attrs[:preferences] ||= {} attrs[:preferences][:align] = FIELD_ALIGNMENT.fetch(field[:Q], 'left') end if field.field_type == :Btn && field.concrete_field_type == :radio_button && field[:Opt].present? selected_option_index = (field.allowed_values || []).find_index(field.field_value) selected_option = field[:Opt][selected_option_index] if selected_option_index { **attrs, type: 'radio', options: build_options(field[:Opt], 'radio'), default_value: selected_option } elsif field.field_type == :Btn && %i[check_box radio_button].include?(field.concrete_field_type) && field[:Kids].present? && field[:Kids].size > 1 && field.allowed_values.size > 1 selected_option = (field.allowed_values || []).find { |v| v == field.field_value } return {} if field.allowed_values.include?(:BBox) { **attrs, type: 'radio', options: build_options(field.allowed_values, 'radio'), default_value: selected_option } elsif field.field_type == :Btn && %i[check_box radio_button].include?(field.concrete_field_type) { **attrs, type: 'checkbox', default_value: field.field_value.present? } elsif field.field_type == :Ch && %i[combo_box editable_combo_box].include?(field.concrete_field_type) && field[:Opt].present? { **attrs, type: 'select', options: build_options(field[:Opt], 'select'), default_value: field.field_value.to_s.match?(SELECT_PLACEHOLDER_REGEXP) ? nil : field.field_value } elsif field.field_type == :Ch && field.concrete_field_type == :multi_select && field[:Opt].present? { **attrs, type: 'multiple', options: build_options(field[:Opt], 'multiple'), default_value: field.field_value } elsif field.field_type == :Tx && field.concrete_field_type == :comb_text_field { **attrs, type: 'cells', default_value: field.field_value } elsif field.field_type == :Tx if field[:AA] && ((field[:AA][:F] && field[:AA][:F][:JS].include?('AFDate_')) || (field[:AA][:K] && field[:AA][:K][:JS].include?('AFDate_'))) if (format = field[:AA][:F][:JS][DATE_FORMAT_REGEXP]) attrs[:preferences] ||= {} attrs[:preferences][:format] = format.upcase end { **attrs, type: 'date', default_value: field.field_value } else { **attrs, type: 'text', default_value: field.field_value } end elsif field.field_type == :Sig { **attrs, type: field.try(:field_name).to_s.downcase.include?('initials') ? 'initials' : 'signature' } else {} end.compact end def build_options(values, type = nil) is_skip_single_value = type.in?(%w[radio multiple]) && values.uniq.size == 1 values.filter_map do |option| is_option_number = option.is_a?(Symbol) && option.to_s.match?(/\A\d+\z/) option = option[1] if option.is_a?(Array) && option.size == 2 option = option.encode('utf-8', invalid: :replace, undef: :replace, replace: '') if option.is_a?(String) next if type == 'select' && option.to_s.match?(SELECT_PLACEHOLDER_REGEXP) { uuid: SecureRandom.uuid, value: is_option_number || is_skip_single_value ? '' : option } end end def build_fields_with_pages(pdf) fields_index = {} annots_index = {} pdf.pages.each do |page| page.each_annotation do |annot| annots_index[annot.hash] = page if !annot.key?(:Parent) && annot.key?(:FT) fields_index[annot.hash] ||= HexaPDF::Type::AcroForm::Field.wrap(pdf, annot) elsif annot.key?(:Parent) field = annot[:Parent] field = field[:Parent] while field[:Parent] annots_index[field.hash] ||= page fields_index[field.hash] ||= HexaPDF::Type::AcroForm::Field.wrap(pdf, field) end end end [process_fields_array(pdf, fields_index.values), annots_index] end def process_fields_array(pdf, array, acc = []) array.each_with_index do |field, index| next if field.nil? unless field.respond_to?(:type) && field.type == :XXAcroFormField array[index] = field = HexaPDF::Type::AcroForm::Field.wrap(pdf, field) end if field.terminal_field? acc << field else process_fields_array(pdf, field[:Kids], acc) end end acc end # rubocop:enable Metrics end end