# -*- 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/error'
require 'hexapdf/type/acro_form/variable_text_field'
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
#
# :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: PDF1.7 s12.7.4.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"
end
str = str.gsub(/[[:space:]]/, ' ') if str && concrete_field_type == :single_line_text_field
if key?(:MaxLen) && str && str.length > self[:MaxLen]
raise HexaPDF::Error, "Value exceeds maximum allowed length of #{self[:MaxLen]}"
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
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
yield("Text contents of field '#{full_field_name}' is too long")
end
if comb_text_field? && !max_len
yield("Comb text field needs a value for /MaxLen")
end
end
end
end
end
end