# -*- 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-2025 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 <http://www.gnu.org/licenses/>.
#
# 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 <https://gettalong.at/hexapdf/>.
#++
require 'hexapdf/error'
require 'hexapdf/type/acro_form/variable_text_field'
require 'hexapdf/type/acro_form/java_script_actions'
module HexaPDF
module Type
module AcroForm
# AcroForm text fields provide a box or space to fill-in data entered from keyboard. The text
# may be restricted to a single line or can span multiple lines.
#
# A special type of single-line text field is the comb text field. This type of field divides
# the existing space into /MaxLen equally spaced positions.
#
# == Type Specific Field Flags
#
# See the class description for Field for the general field flags.
#
# :multiline:: If set, the text field may contain multiple lines.
#
# :password:: The field is a password field. This changes the behaviour of the PDF reader
# application to not echo the input text and to not store it in the PDF file.
#
# :file_select:: The text field represents a file selection control where the input text is
# the path to a file.
#
# :do_not_spell_check:: The text should not be spell-checked.
#
# :do_not_scroll:: The text field should not scroll (horizontally for single-line fields and
# vertically for multiline fields) to accomodate more text than fits into the
# annotation rectangle. This means that no more text can be entered once the
# field is full.
#
# :comb:: The field is divided into /MaxLen equally spaced positions (so /MaxLen needs to be
# set). This is useful, for example, when entering things like social security
# numbers which always have the same length.
#
# :rich_text:: The field is a rich text field.
#
# See: PDF2.0 s12.7.5.3
class TextField < VariableTextField
define_type :XXAcroFormField
define_field :MaxLen, type: Integer
# All inheritable dictionary fields for text fields.
INHERITABLE_FIELDS = (superclass::INHERITABLE_FIELDS + [:MaxLen]).freeze
# Updated list of field flags.
FLAGS_BIT_MAPPING = superclass::FLAGS_BIT_MAPPING.merge(
{
multiline: 12,
password: 13,
file_select: 20,
do_not_spell_check: 22,
do_not_scroll: 23,
comb: 24,
rich_text: 25,
}
).freeze
# Initializes the text field to be a multiline text field.
#
# This method should only be called directly after creating a new text field because it
# doesn't completely reset the object.
def initialize_as_multiline_text_field
flag(:multiline)
unflag(:file_select, :comb, :password)
end
# Initializes the text field to be a comb text field.
#
# This method should only be called directly after creating a new text field because it
# doesn't completely reset the object.
def initialize_as_comb_text_field
flag(:comb)
unflag(:file_select, :multiline, :password)
end
# Initializes the text field to be a password field.
#
# This method should only be called directly after creating a new text field because it
# doesn't completely reset the object.
def initialize_as_password_field
delete(:V)
flag(:password)
unflag(:comb, :multiline, :file_select)
end
# Initializes the text field to be a file select field.
#
# This method should only be called directly after creating a new text field because it
# doesn't completely reset the object.
def initialize_as_file_select_field
flag(:file_select)
unflag(:comb, :multiline, :password)
end
# Returns +true+ if this field is a multiline text field.
def multiline_text_field?
flagged?(:multiline) && !(flagged?(:file_select) || flagged?(:comb) || flagged?(:password))
end
# Returns +true+ if this field is a comb text field.
def comb_text_field?
flagged?(:comb) && !(flagged?(:file_select) || flagged?(:multiline) || flagged?(:password))
end
# Returns +true+ if this field is a password field.
def password_field?
flagged?(:password) && !(flagged?(:file_select) || flagged?(:multiline) || flagged?(:comb))
end
# Returns +true+ if this field is a file select field.
def file_select_field?
flagged?(:file_select) && !(flagged?(:password) || flagged?(:multiline) || flagged?(:comb))
end
# Returns the field value, i.e. the text contents of the field, or +nil+ if no value is set.
#
# Note that modifying the returned value *might not* modify the text contents in case it is
# stored as stream! So always use #field_value= to set the field value.
def field_value
return unless value[:V]
self[:V].kind_of?(String) ? self[:V] : self[:V].stream
end
# Sets the field value, i.e. the text contents of the field, to the given string.
#
# Note that for single line text fields, all whitespace characters are changed to simple
# spaces.
def field_value=(str)
if flagged?(:password)
raise HexaPDF::Error, "Storing a field value for a password field is not allowed"
elsif comb_text_field? && !key?(:MaxLen)
raise HexaPDF::Error, "A comb text field need a valid /MaxLen value"
elsif str && !str.kind_of?(String)
str = @document.config['acro_form.on_invalid_value'].call(self, str)
end
str = str.gsub(/[[:space:]]/, ' ') if str && concrete_field_type == :single_line_text_field
if key?(:MaxLen) && str && str.length > self[:MaxLen]
str = @document.config['acro_form.text_field.on_max_len_exceeded'].call(self, str)
end
self[:V] = str
update_widgets
end
# Returns the default field value.
#
# See: #field_value
def default_field_value
self[:DV].kind_of?(String) ? self[:DV] : self[:DV].stream
end
# Sets the default field value.
#
# See: #field_value=
def default_field_value=(str)
self[:DV] = str
end
# Returns the concrete text field type, either :single_line_text_field,
# :multiline_text_field, :password_field, :file_select_field, :comb_text_field or
# :rich_text_field.
def concrete_field_type
if flagged?(:multiline)
:multiline_text_field
elsif flagged?(:password)
:password_field
elsif flagged?(:file_select)
:file_select_field
elsif flagged?(:comb)
:comb_text_field
elsif flagged?(:rich_text)
:rich_text_field
else
:single_line_text_field
end
end
# Creates appropriate appearances for all widgets.
#
# For information on how this is done see AppearanceGenerator.
#
# Note that no new appearances are created if the field value hasn't changed between
# invocations.
#
# By setting +force+ to +true+ the creation of the appearances can be forced.
def create_appearances(force: false)
current_value = field_value
appearance_generator_class = document.config.constantize('acro_form.appearance_generator')
each_widget do |widget|
is_cached = widget.cached?(:last_value)
unless force
if is_cached && widget.cache(:last_value) == current_value
next
elsif !is_cached && widget.appearance?
widget.cache(:last_value, current_value, update: true)
next
end
end
widget.cache(:last_value, current_value, update: true)
appearance_generator_class.new(widget).create_text_appearances
end
end
# Updates the widgets so that they reflect the current field value.
def update_widgets
create_appearances(force: true)
end
# Sets the specified JavaScript format action on the field's widgets.
#
# This action is executed when the field value needs to be formatted for rendering in the
# appearance streams of the associated widgets.
#
# The argument +type+ can be one of the following:
#
# :number::
# Assumes that the field value is a number and formats it according to the given
# arguments. See JavaScriptActions.af_number_format_action for details on the arguments.
#
# :percent::
# Assumes that the field value is a number and formats it as percentage (where 1=100%
# and 0=0%). See JavaScriptActions.af_percent_format_action for details on the
# arguments.
#
# :time::
# Assumes that the field value is a string with a time value and formats it according to
# the given argument. See JavaScriptActions.af_time_format_action for details on the
# arguments.
def set_format_action(type, **arguments)
action_string = case type
when :number then JavaScriptActions.af_number_format_action(**arguments)
when :percent then JavaScriptActions.af_percent_format_action(**arguments)
when :time then JavaScriptActions.af_time_format_action(**arguments)
else
raise ArgumentError, "Invalid value for type argument: #{type.inspect}"
end
self[:AA] ||= {}
self[:AA][:F] = {S: :JavaScript, JS: action_string}
end
# Sets the specified JavaScript calculate action on the field.
#
# This action is executed by a viewer when any field's value changes so as to recalculate
# the value of this field. Usually, the field is also flagged as read only to avoid a user
# changing the value manually.
#
# Note that HexaPDF *doesn't* automatically recalculate field values, use
# Form#recalculate_fields to manually kick off recalculation.
#
# The argument +type+ can be one of the following:
#
# :sum::
# Sums the values of the given +fields+.
#
# :average::
# Calculates the average value of the given +fields+.
#
# :product::
# Multiplies the values of the given +fields+.
#
# :min::
# Uses the minimum value of the given +fields+.
#
# :max::
# Uses the maximum value of the given +fields+.
#
# :sfn::
# Uses the Simplified Field Notation for calculating the field's value. This allows for
# more complex calculations involving addition, subtraction, multiplication and
# division. Field values are specified by using the full field names which should not
# contain spaces or punctuation characters except point.
#
# The +fields+ argument needs to contain a string with the SFN calculation rule.
#
# Here are some examples:
#
# field1 + field2 - field3
# (field.1 + field.2) * (field.3 - field.4)
#
# Note: Setting this action appends the field to the main Form's /CO entry which specifies
# the calculation order. Rearrange the entries as needed.
def set_calculate_action(type, fields: nil)
action_string = case type
when :sum, :average, :product, :min, :max
JavaScriptActions.af_simple_calculate_action(type, fields)
when :sfn
JavaScriptActions.simplified_field_notation_action(document.acro_form, fields)
else
raise ArgumentError, "Invalid value for type argument: #{type.inspect}"
end
self[:AA] ||= {}
self[:AA][:C] = {S: :JavaScript, JS: action_string}
(document.acro_form[:CO] ||= []) << self
end
private
def perform_validation #:nodoc:
if field_type != :Tx
yield("Field /FT of AcroForm text field has to be :Tx", true)
self[:FT] = :Tx
end
super
if self[:V] && !(self[:V].kind_of?(String) || self[:V].kind_of?(HexaPDF::Stream))
yield("Text field doesn't contain text but #{self[:V].class} object")
return
end
if (max_len = self[:MaxLen]) && field_value && field_value.length > max_len
correctable = true
begin
str = @document.config['acro_form.text_field.on_max_len_exceeded'].call(self, field_value)
rescue HexaPDF::Error
correctable = false
end
yield("Text contents of field '#{full_field_name}' is too long", correctable)
self.field_value = str if correctable
end
if comb_text_field? && !max_len
yield("Comb text field needs a value for /MaxLen")
end
end
end
end
end
end