# -*- 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/type/acro_form/variable_text_field'
require 'hexapdf/type/acro_form/appearance_generator'
module HexaPDF
module Type
module AcroForm
# AcroForm choice fields contain multiple text items of which one (or, if so flagged, more)
# may be selected.
#
# They are divided into scrollable list boxes and combo boxes. To create a list or combo box,
# use the appropriate convenience methods on the main Form instance
# (HexaPDF::Document#acro_form). By using those methods, everything needed is automatically
# set up.
#
# == Type Specific Field Flags
#
# :combo:: If set, the field represents a comb box.
#
# :edit:: If set, the combo box includes an editable text box for entering arbitrary values.
# Therefore the 'combo' flag also needs to be set.
#
# :sort:: The option items have to be sorted alphabetically. This flag is intended for PDF
# writers, not readers which should display the items in the order they appear.
#
# :multi_select:: If set, more than one item may be selected.
#
# :do_not_spell_check:: The text should not be spell-checked.
#
# :commit_on_sel_change:: If set, a new value should be commited as soon as a selection is
# made.
#
# See: PDF1.7 s12.7.4.4
class ChoiceField < VariableTextField
define_field :Opt, type: PDFArray
define_field :TI, type: Integer, default: 0
define_field :I, type: PDFArray, version: '1.4'
# Updated list of field flags.
FLAGS_BIT_MAPPING = superclass::FLAGS_BIT_MAPPING.merge(
{
combo: 17,
edit: 18,
sort: 19,
multi_select: 21,
do_not_spell_check: 22,
commit_on_sel_change: 26,
}
).freeze
# Initializes the choice field to be a list box.
#
# This method should only be called directly after creating a new choice field because it
# doesn't completely reset the object.
def initialize_as_list_box
self[:V] = nil
unflag(:combo)
end
# Initializes the button field to be a combo box.
#
# This method should only be called directly after creating a new choice field because it
# doesn't completely reset the object.
def initialize_as_combo_box
self[:V] = nil
flag(:combo)
end
# Returns +true+ if this choice field represents a list box.
def list_box?
!combo_box?
end
# Returns +true+ if this choice field represents a combo box.
def combo_box?
flagged?(:combo)
end
# Returns the field value which represents the currently selected item(s).
#
# If no item is selected, +nil+ is returned. If multiple values are selected, the return
# value is an array of strings, otherwise it is just a string.
def field_value
process_value(self[:V])
end
# Sets the field value to the given string or array of strings.
#
# The dictionary field /I is also modified to correctly represent the selected item(s).
def field_value=(value)
items = option_items
array_value = [value].flatten
all_included = array_value.all? {|v| items.include?(v) }
self[:V] = if combo_box? && value.kind_of?(String) &&
(flagged?(:edit) || all_included)
delete(:I)
value
elsif list_box? && all_included &&
(value.kind_of?(String) || flagged?(:multi_select))
self[:I] = array_value.map {|val| items.index(val) }.sort!
array_value.length == 1 ? value : array_value
else
@document.config['acro_form.on_invalid_value'].call(self, value)
end
update_widgets
end
# Returns the default field value.
#
# See: #field_value
def default_field_value
process_value(self[:DV])
end
# Sets the default field value.
#
# See: #field_value=
def default_field_value=(value)
items = option_items
self[:DV] = if [value].flatten.all? {|v| items.include?(v) }
value
else
@document.config['acro_form.on_invalid_value'].call(self, value)
end
end
# Returns the array with the available option items.
#
# Note that this *only* returns the option items themselves! For getting the export values,
# the #export_values method has to be used.
def option_items
key?(:Opt) ? process_value(self[:Opt].map {|i| i.kind_of?(Array) ? i[1] : i }) : []
end
# Returns the export values of the option items.
#
# If you need the display strings (as in most cases), use the #option_items method.
def export_values
key?(:Opt) ? process_value(self[:Opt].map {|i| i.kind_of?(Array) ? i[0] : i }) : []
end
# Sets the array with the available option items to the given value.
#
# Each entry in the array may either be a string representing the text to be displayed. Or
# an array of two strings where the first describes the export value (to be used when
# exporting form field data from the document) and the second is the display value.
#
# See: #option_items, #export_values
def option_items=(value)
self[:Opt] = if flagged?(:sort)
value.sort_by {|i| process_value(i.kind_of?(Array) ? i[1] : i) }
else
value
end
end
# Returns the index of the first visible option item of a list box.
def list_box_top_index
self[:TI]
end
# Makes the option item referred to via the given +index+ the first visible option item of a
# list box.
def list_box_top_index=(index)
if index < 0 || !key?(:Opt) || index >= self[:Opt].length
raise ArgumentError, "Index out of range for the set option items"
end
self[:TI] = index
end
# Returns the concrete choice field type, either :list_box, :combo_box or
# :editable_combo_box.
def concrete_field_type
if combo_box?
flagged?(:edit) ? :editable_combo_box : :combo_box
else
:list_box
end
end
# Creates appropriate appearances for all widgets if they don't already exist.
#
# For information on how this is done see AppearanceGenerator.
#
# Note that no new appearances are created if the dictionary fields involved in the creation
# of the appearance stream have not been changed between invocations.
#
# By setting +force+ to +true+ the creation of the appearances can be forced.
def create_appearances(force: false)
current_appearance_state = [self[:V], self[:I], self[:Opt], self[:TI]]
appearance_generator_class = document.config.constantize('acro_form.appearance_generator')
each_widget do |widget|
next if !force && widget.cached?(:appearance_state) &&
widget.cache(:appearance_state) == current_appearance_state
widget.cache(:appearance_state, current_appearance_state, update: true)
if combo_box?
appearance_generator_class.new(widget).create_combo_box_appearances
else
appearance_generator_class.new(widget).create_list_box_appearances
end
end
end
# Updates the widgets so that they reflect the current field value.
def update_widgets
create_appearances
end
private
# Uses the HexaPDF::DictionaryFields::StringConverter to process the value (a string or an
# array of strings) so that it contains only normalized strings.
def process_value(value)
value = value.value if value.kind_of?(PDFArray)
if value.kind_of?(Array)
value.map! {|item| DictionaryFields::StringConverter.convert(item, nil, nil) || item }
else
DictionaryFields::StringConverter.convert(value, nil, nil) || value
end
end
def perform_validation #:nodoc:
if field_type != :Ch
yield("Field /FT of AcroForm choie field has to be :Ch", true)
self[:FT] = :Ch
end
super
end
end
end
end
end