# -*- 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-2022 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/error' require 'hexapdf/utils/bit_field' require 'hexapdf/type/annotations' module HexaPDF module Type module AcroForm # AcroForm field dictionaries are used to define the properties of form fields of AcroForm # objects. # # Fields can be organized in a hierarchy using the /Kids and /Parent keys, for namespacing # purposes and to set default values. Those fields that have other fields as children are # called non-terminal fields, otherwise they are called terminal fields. # # While field objects can be created manually, it is best to use the various +create_+ methods # of the main Form object to create them so that all necessary things are set up correctly. # # == Field Types # # Subclasses are used to implement the specific AcroForm field types: # # * ButtonField implements the button fields (pushbuttons, check boxes and radio buttons) # * TextField implements single or multiline text fields. # * ChoiceField implements scrollable list boxes or (editable) combo boxes. # * SignatureField implements signature fields. # # == Field Flags # # Various characteristics of a field can be changed by setting a certain flag. Some flags are # defined for all types of field, some are specific to a certain type. # # The following flags apply to all fields: # # :read_only:: The field is read only which means the user can't change the value or interact # with associated widget annotations. # # :required:: The field is required if the form is exported by a submit-form action. # # :no_export:: The field should *not* be exported by a submit-form action. # # == Field Type Implementation Notes # # If an AcroForm field type adds additional inheritable dictionary fields, it has to set the # constant +INHERITABLE_FIELDS+ to all inheritable dictionary fields, including those from the # superclass. # # Similarily, if additional flags are provided, the constant +FLAGS_BIT_MAPPING+ has to be set # to combination of the superclass value of the constant and the mapping of flag names to bit # indices. # # See: PDF1.7 s12.7.3.1 class Field < Dictionary # Provides a #value method for hash that returns self so that a Hash can be used # interchangably with a HexaPDF::Dictionary. module HashRefinement refine Hash do def value self end end end using HashRefinement extend Utils::BitField define_type :XXAcroFormField define_field :FT, type: Symbol, allowed_values: [:Btn, :Tx, :Ch, :Sig] define_field :Parent, type: :XXAcroFormField define_field :Kids, type: PDFArray define_field :T, type: String define_field :TU, type: String, version: '1.3' define_field :TM, type: String, version: '1.3' define_field :Ff, type: Integer, default: 0 define_field :V, type: [Dictionary, Symbol, String, Stream, PDFArray] define_field :DV, type: [Dictionary, Symbol, String, Stream, PDFArray] define_field :AA, type: Dictionary, version: '1.2' # The inheritable dictionary fields common to all AcroForm field types. INHERITABLE_FIELDS = [:FT, :Ff, :V, :DV].freeze ## # :method: flags # # Returns an array of flag names representing the set bit flags. # ## # :method: flagged? # :call-seq: # flagged?(flag) # # Returns +true+ if the given flag is set. The argument can either be the flag name or the # bit index. # ## # :method: flag # :call-seq: # flag(*flags, clear_existing: false) # # Sets the given flags, given as flag names or bit indices. If +clear_existing+ is +true+, # all prior flags will be cleared. # bit_field(:flags, {read_only: 0, required: 1, no_export: 2}, lister: "flags", getter: "flagged?", setter: "flag", unsetter: "unflag", value_getter: "self[:Ff]", value_setter: "self[:Ff]") # Treats +name+ as an inheritable dictionary field and resolves its value for the AcroForm # field +field+. def self.inherited_value(field, name) while field.value[name].nil? && (parent = field[:Parent]) field = parent end field.value[name].nil? ? nil : field[name] end # Form fields must always be indirect objects. def must_be_indirect? true end # Returns the value for the entry +name+. # # If +name+ is an inheritable field and the value has not been set on this field object, its # value is retrieved from the parent fields. # # See: Dictionary#[] def [](name) if value[name].nil? && self.class::INHERITABLE_FIELDS.include?(name) self.class.inherited_value(self, name) || super else super end end # Returns the type of the field, either :Btn (pushbuttons, check boxes, radio buttons), :Tx # (text fields), :Ch (scrollable list boxes, combo boxes) or :Sig (signature fields). # # Also see #concrete_field_type def field_type self[:FT] end # Returns the concrete field type (:button_field, :text_field, :choice_field or # :signature_field) or +nil+ is no field type is set. # # In constrast to #field_type this method also considers the field flags and not just the # field type. This means that subclasses can return a more concrete name for the field type. # # Also see #field_type def concrete_field_type case self[:FT] when :Btn then :button_field when :Tx then :text_field when :Ch then :choice_field when :Sig then :signature_field else nil end end # Returns the name of the field or +nil+ if no name is set. def field_name self[:T] end # Returns the full name of the field or +nil+ if no name is set. # # The full name of a field is constructed using the full name of the parent field, a period # and the field name of the field. def full_field_name if key?(:Parent) [self[:Parent].full_field_name, field_name].compact.join('.') else field_name end end # Returns the alternate field name that should be used for display purposes (e.g. Acrobat # shows this as tool tip). def alternate_field_name self[:TU] end # Sets the alternate field name. # # See #alternate_field_name def alternate_field_name=(value) self[:TU] = value end # Returns +true+ if this is a terminal field. def terminal_field? kids = self[:Kids] # PDF 2.0 s12.7.4.2 clarifies how to do check for fields since PDF 1.7 isn't clear kids.nil? || kids.empty? || kids.none? {|kid| kid.key?(:T) } end # Returns +true+ if the field contains an embedded widget. def embedded_widget? key?(:Subtype) end # :call-seq: # field.each_widget {|widget| block} -> field # field.each_widget -> Enumerator # # Yields each widget, i.e. visual representation, of this field. # # Widgets can be associated to the field in three ways: # # 1. The widget can be embedded in the field itself. # 2. One or more widgets are defined as children of this field. # 3. Widgets of *another field instance with the same full field name*. # # Because of possibility 3 all fields of the form have to be searched to check whether there # is another field with the same full field name. # # See: HexaPDF::Type::Annotations::Widget def each_widget(direct_only: false, &block) # :yields: widget return to_enum(__method__, direct_only: direct_only) unless block_given? if embedded_widget? yield(document.wrap(self)) elsif terminal_field? self[:Kids]&.each {|kid| yield(document.wrap(kid)) } end unless direct_only my_name = full_field_name document.acro_form&.each_field do |field| next if field.full_field_name != my_name || field == self field.each_widget(direct_only: true, &block) end end self end # Creates a new widget annotation for this form field (must be a terminal field!) on the # given +page+, adding the +values+ to the created widget annotation oject. # # If +allow_embedded+ is +false+, embedding the first widget in the field itself is not # allowed. # # The +values+ argument should at least include :Rect for setting the visible area of the # widget. # # If the field already has an embedded widget, i.e. field and widget are the same PDF # object, its widget data is extracted to a new PDF object and stored in the /Kids field, # together with the new widget annotation. Note that this means that a possible reference to # the formerly embedded widget (=this field) is not valid anymore! # # See: HexaPDF::Type::Annotations::Widget def create_widget(page, allow_embedded: true, **values) unless terminal_field? raise HexaPDF::Error, "Widgets can only be added to terminal fields" end widget_data = {Type: :Annot, Subtype: :Widget, Rect: [0, 0, 0, 0], **values} if !allow_embedded || embedded_widget? || (key?(:Kids) && !self[:Kids].empty?) kids = self[:Kids] ||= [] kids << extract_widget if embedded_widget? widget = document.add(widget_data) widget[:Parent] = self self[:Kids] << widget else value.update(widget_data) widget = document.wrap(self) end widget.flag(:print) widget[:P] = page (page[:Annots] ||= []) << widget widget end # Deletes the given widget annotation object from this field, the page it appears on and the # document. # # If the given widget is not a widget of this field, nothing is done. def delete_widget(widget) widget = if embedded_widget? && self == widget widget elsif terminal_field? (widget_index = self[:Kids]&.index(widget)) && widget end return unless widget document.pages.each do |page| break if page[:Annots]&.delete(widget) # See comment in #extract_widget end if embedded_widget? WIDGET_FIELDS.each {|key| delete(key) } document.revisions.each {|revision| break if revision.update(self) } else self[:Kids].delete_at(widget_index) document.delete(widget) end end private # An array of all widget annotation field names. WIDGET_FIELDS = HexaPDF::Type::Annotations::Widget.each_field.map(&:first).uniq - [:Parent] # Returns a new dictionary object with all the widget annotation data that is stored # directly in the field and adjust the references accordingly. If the field doesn't have any # widget data, +nil+ is returned. def extract_widget return unless embedded_widget? data = WIDGET_FIELDS.each_with_object({}) do |key, hash| hash[key] = delete(key) if key?(key) end widget = document.add(data, type: :Annot) widget[:Parent] = self document.pages.each do |page| if page.key?(:Annots) && (index = page[:Annots].index(self)) page[:Annots][index] = widget break # Each annotation dictionary may only appear on one page, see PDF1.7 12.5.2 end end widget end def perform_validation #:nodoc: super if terminal_field? && field_type.nil? yield("/FT is required for terminal fields") end if key?(:T) && self[:T].include?('.') yield("/T shall not contain a period") end end end end end end