# -*- 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