# -*- encoding: utf-8; frozen_string_literal: true -*- # #-- # This file is part of HexaPDF. # # HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby # Copyright (C) 2014-2021 Thomas Leitner # # HexaPDF is free software: you can redistribute it and/or modify it # under the terms of the GNU Affero General Public License version 3 as # published by the Free Software Foundation with the addition of the # following permission added to Section 15 as permitted in Section 7(a): # FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY # THOMAS LEITNER, THOMAS LEITNER DISCLAIMS THE WARRANTY OF NON # INFRINGEMENT OF THIRD PARTY RIGHTS. # # HexaPDF is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # License for more details. # # You should have received a copy of the GNU Affero General Public License # along with HexaPDF. If not, see . # # The interactive user interfaces in modified source and object code # versions of HexaPDF must display Appropriate Legal Notices, as required # under Section 5 of the GNU Affero General Public License version 3. # # In accordance with Section 7(b) of the GNU Affero General Public # License, a covered work must retain the producer line in every PDF that # is created or manipulated using HexaPDF. # # If the GNU Affero General Public License doesn't fit your need, # commercial licenses are available at . #++ require 'hexapdf/dictionary' require 'hexapdf/stream' require 'hexapdf/type/acro_form/field' require 'hexapdf/utils/bit_field' module HexaPDF module Type module AcroForm # Represents the PDF's interactive form dictionary. It is linked from the catalog dictionary # via the /AcroForm entry. # # == Overview # # An interactive form consists of fields which can be structured hierarchically and shown on # pages by using Annotations::Widget annotations. This means one field can have zero, one or # more visual representations on one or more pages. The fields at the bottom of the hierarchy # which have no parent are called "root fields" and are stored in /Fields. # # Each field in a form has a certain type which determines how it should be displayed and what # a user can do with it. The most common type is "text field" which allows the user to enter # one or more lines of text. There are also check boxes, radio buttons, list boxes and combo # boxes. # # == Visual Appearance # # The visual appearance of a field is normally provided by the application creating the PDF. # This is done by generating the so called appearances for all widgets of a field. However, it # is also possible to instruct the PDF reader application to generate the appearances on the # fly using the /NeedAppearances key, see #need_appearances!. # # HexaPDF uses the configuration option +acro_form.create_appearance_streams+ to determine # whether appearances should automatically be generated. # # See: PDF1.7 s12.7.2, Field, HexaPDF::Type::Annotations::Widget class Form < Dictionary extend Utils::BitField define_type :XXAcroForm define_field :Fields, type: PDFArray, required: true, version: '1.2' define_field :NeedAppearances, type: Boolean, default: false define_field :SigFlags, type: Integer, version: '1.3' define_field :CO, type: PDFArray, version: '1.3' define_field :DR, type: :XXResources define_field :DA, type: String define_field :XFA, type: [Stream, PDFArray], version: '1.5' bit_field(:raw_signature_flags, {signatures_exist: 0, append_only: 1}, lister: "signature_flags", getter: "signature_flag?", setter: "signature_flag", unsetter: "signature_unflag") # Returns the PDFArray containing the root fields. def root_fields self[:Fields] ||= document.wrap([]) end # Returns an array with all root fields that were found in the PDF document. def find_root_fields result = [] document.pages.each do |page| page.each_annotation do |annot| if !annot.key?(:Parent) && annot.key?(:FT) result << document.wrap(annot, type: :XXAcroFormField, subtype: annot[:FT]) elsif annot.key?(:Parent) field = annot[:Parent] field = field[:Parent] while field[:Parent] result << document.wrap(field, type: :XXAcroFormField) end end end result end # Finds all root fields and sets /Fields appropriately. # # See: #find_root_fields def find_root_fields! self[:Fields] = find_root_fields end # :call-seq: # acroform.each_field(terminal_only: true) {|field| block} -> acroform # acroform.each_field(terminal_only: true) -> Enumerator # # Yields all terminal fields or all fields, depending on the +terminal_only+ argument. def each_field(terminal_only: true) return to_enum(__method__, terminal_only: terminal_only) unless block_given? process_field = lambda do |field| field = document.wrap(field, type: :XXAcroFormField, subtype: Field.inherited_value(field, :FT)) yield(field) if field.terminal_field? || !terminal_only field[:Kids].each(&process_field) unless field.terminal_field? end root_fields.each(&process_field) self end # Returns the field with the given +name+ or +nil+ if no such field exists. def field_by_name(name) fields = root_fields field = nil name.split('.').each do |part| field = fields&.find {|f| f[:T] == part } break unless field field = document.wrap(field, type: :XXAcroFormField, subtype: Field.inherited_value(field, :FT)) fields = field[:Kids] unless field.terminal_field? end field end # Creates a new text field with the given name and adds it to the form. # # The +name+ may contain dots to signify a field hierarchy. If so, the referenced parent # fields must already exist. If it doesn't contain dots, a top-level field is created. # # The optional keyword arguments allow setting often used properties of the field: # # +font+:: # The font that should be used for the text of the field. If +font_size+ or # +font_options+ is specified but +font+ isn't, the font Helvetica is used. # # If no font is set on the text field, the default font properties of the AcroForm form # are used. Note that field specific or form specific font properties have to be set. # Otherwise there will be an error when trying to generate a visual representation of # the field value. # # +font_options+:: # A hash with font options like :variant that should be used. # # +font_size+:: # The font size that should be used. If +font+ or +font_options+ is specified but # +font_size+ isn't, font size defaults to 0 (= auto-sizing). # # +align+:: # The alignment of the text, either :left, :center or :right. def create_text_field(name, font: nil, font_options: nil, font_size: nil, align: nil) create_field(name, :Tx) do |field| apply_variable_text_properties(field, font: font, font_options: font_options, font_size: font_size, align: align) end end # Creates a new multiline text field with the given name and adds it to the form. # # The +name+ may contain dots to signify a field hierarchy. If so, the referenced parent # fields must already exist. If it doesn't contain dots, a top-level field is created. # # The optional keyword arguments allow setting often used properties of the field, see # #create_text_field for details. def create_multiline_text_field(name, font: nil, font_options: nil, font_size: nil, align: nil) create_field(name, :Tx) do |field| field.initialize_as_multiline_text_field apply_variable_text_properties(field, font: font, font_options: font_options, font_size: font_size, align: align) end end # Creates a new comb text field with the given name and adds it to the form. # # The +max_chars+ argument defines the maximum number of characters the comb text field can # accommodate. # # The +name+ may contain dots to signify a field hierarchy. If so, the referenced parent # fields must already exist. If it doesn't contain dots, a top-level field is created. # # The optional keyword arguments allow setting often used properties of the field, see # #create_text_field for details. def create_comb_text_field(name, max_chars:, font: nil, font_options: nil, font_size: nil, align: nil) create_field(name, :Tx) do |field| field.initialize_as_comb_text_field apply_variable_text_properties(field, font: font, font_options: font_options, font_size: font_size, align: align) field[:MaxLen] = max_chars end end # Creates a new file select field with the given name and adds it to the form. # # The +name+ may contain dots to signify a field hierarchy. If so, the referenced parent # fields must already exist. If it doesn't contain dots, a top-level field is created. # # The optional keyword arguments allow setting often used properties of the field, see # #create_text_field for details. def create_file_select_field(name, font: nil, font_options: nil, font_size: nil, align: nil) create_field(name, :Tx) do |field| field.initialize_as_file_select_field apply_variable_text_properties(field, font: font, font_options: font_options, font_size: font_size, align: align) end end # Creates a new password field with the given name and adds it to the form. # # The +name+ may contain dots to signify a field hierarchy. If so, the referenced parent # fields must already exist. If it doesn't contain dots, a top-level field is created. # # The optional keyword arguments allow setting often used properties of the field, see # #create_text_field for details. def create_password_field(name, font: nil, font_options: nil, font_size: nil, align: nil) create_field(name, :Tx) do |field| field.initialize_as_password_field apply_variable_text_properties(field, font: font, font_options: font_options, font_size: font_size, align: align) end end # Creates a new check box with the given name and adds it to the form. # # The +name+ may contain dots to signify a field hierarchy. If so, the referenced parent # fields must already exist. If it doesn't contain dots, a top-level field is created. def create_check_box(name) create_field(name, :Btn, &:initialize_as_check_box) end # Creates a radio button with the given name and adds it to the form. # # The +name+ may contain dots to signify a field hierarchy. If so, the referenced parent # fields must already exist. If it doesn't contain dots, a top-level field is created. def create_radio_button(name) create_field(name, :Btn, &:initialize_as_radio_button) end # Creates a combo box with the given name and adds it to the form. # # The +name+ may contain dots to signify a field hierarchy. If so, the referenced parent # fields must already exist. If it doesn't contain dots, a top-level field is created. # # The optional keyword arguments allow setting often used properties of the field: # # +option_items+:: # Specifies the values of the list box. # # +editable+:: # If set to +true+, the combo box allows entering an arbitrary value in addition to # selecting one of the provided option items. # # +font+, +font_options+, +font_size+ and +align+:: # See #create_text_field def create_combo_box(name, option_items: nil, editable: nil, font: nil, font_options: nil, font_size: nil, align: nil) create_field(name, :Ch) do |field| field.initialize_as_combo_box field.option_items = option_items if option_items field.flag(:edit) if editable apply_variable_text_properties(field, font: font, font_options: font_options, font_size: font_size, align: align) end end # Creates a list box with the given name and adds it to the form. # # The +name+ may contain dots to signify a field hierarchy. If so, the referenced parent # fields must already exist. If it doesn't contain dots, a top-level field is created. # # The optional keyword arguments allow setting often used properties of the field: # # +option_items+:: # Specifies the values of the list box. # # +multi_select+:: # If set to +true+, the list box allows selecting multiple items instead of only one. # # +font+, +font_options+, +font_size+ and +align+:: # See #create_text_field. def create_list_box(name, option_items: nil, multi_select: nil, font: nil, font_options: nil, font_size: nil, align: nil) create_field(name, :Ch) do |field| field.initialize_as_list_box field.option_items = option_items if option_items field.flag(:multi_select) if multi_select apply_variable_text_properties(field, font: font, font_options: font_options, font_size: font_size, align: align) end end # Returns the dictionary containing the default resources for form field appearance streams. def default_resources self[:DR] ||= document.wrap({ProcSet: [:PDF, :Text, :ImageB, :ImageC, :ImageI]}, type: :XXResources) end # Sets the global default appearance string using the provided values. # # The default argument values are a sane default. If +font_size+ is set to 0, the font size # is calculated using the height/width of the field. def set_default_appearance_string(font: 'Helvetica', font_size: 0) name = default_resources.add_font(document.fonts.add(font).pdf_object) self[:DA] = "0 g /#{name} #{font_size} Tf" end # Sets the /NeedAppearances field to +true+. # # This will make PDF reader applications generate appropriate appearance streams based on # the information stored in the fields and associated widgets. def need_appearances! self[:NeedAppearances] = true end # Creates the appearances for all widgets of all terminal fields if they don't exist. # # If +force+ is +true+, new appearances are created even if there are existing ones. def create_appearances(force: false) each_field do |field| field.create_appearances(force: force) if field.respond_to?(:create_appearances) end end # Flattens the whole interactive form or only the given fields, and returns the fields that # couldn't be flattened. # # Flattening means making the appearance streams of the field widgets part of the respective # page's content stream and removing the fields themselves. # # If the whole interactive form is flattened, the form object itself is also removed if all # fields were flattened. # # The +create_appearances+ argument controls whether missing appearances should # automatically be created. # # See: HexaPDF::Type::Page#flatten_annotations def flatten(fields: nil, create_appearances: true) remove_form = fields.nil? fields ||= each_field.to_a if create_appearances fields.each {|field| field.create_appearances if field.respond_to?(:create_appearances) } end not_flattened = fields.map {|field| field.each_widget.to_a }.flatten document.pages.each {|page| not_flattened = page.flatten_annotations(not_flattened) } fields -= not_flattened.map(&:form_field) fields.each do |field| (field[:Parent]&.[](:Kids) || self[:Fields]).delete(field) document.delete(field) end if remove_form && not_flattened.empty? document.catalog.delete(:AcroForm) document.delete(self) end not_flattened end private # Helper method for bit field getter access. def raw_signature_flags self[:SigFlags] end # Helper method for bit field setter access. def raw_signature_flags=(value) self[:SigFlags] = value end # Creates a new field with the full name +name+ and the field type +type+. def create_field(name, type) parent_name, _, name = name.rpartition('.') parent_field = parent_name.empty? ? nil : field_by_name(parent_name) if !parent_name.empty? && !parent_field raise HexaPDF::Error, "Parent field '#{parent_name}' not found" end field = document.add({FT: type, T: name, Parent: parent_field}, type: :XXAcroFormField, subtype: type) if parent_field (parent_field[:Kids] ||= []) << field else (self[:Fields] ||= []) << field end yield(field) field end # Applies the given variable field properties to the field. def apply_variable_text_properties(field, font: nil, font_options: nil, font_size: nil, align: nil) if font || font_options || font_size field.set_default_appearance_string(font: font || 'Helvetica', font_options: font_options || {}, font_size: font_size || 0) end field.text_alignment(align) if align end def perform_validation # :nodoc: super if (da = self[:DA]) unless self[:DR] yield("When the field /DA is present, the field /DR must also be present") return end font_name, _ = VariableTextField.parse_appearance_string(da) if font_name && !(self[:DR][:Font] && self[:DR][:Font][font_name]) yield("The font specified in /DA is not in the /DR resource dictionary") end else set_default_appearance_string end create_appearances if document.config['acro_form.create_appearances'] end end end end end