# -*- 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-2020 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/field'
require 'hexapdf/type/acro_form/appearance_generator'
module HexaPDF
module Type
module AcroForm
# AcroForm button fields represent interactive controls to be used with the mouse.
#
# They are divided into push buttons (things to click on), check boxes and radio buttons. All
# of these are represented with this class.
#
# To create a push button, check box or radio button field, 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
#
# :no_toggle_to_off:: Only used with radio buttons fields. If this flag is set, one button
# needs to be selected at all times. Otherwise, clicking on the selected
# button deselects it.
#
# :radio:: If this flag is set, the field is a set of radio buttons. Otherwise it is a check
# box. Additionally, the :pushbutton flag needs to be clear.
#
# :push_button:: The field represents a pushbutton without a permanent value.
#
# :radios_in_unison:: A group of radio buttons with the same value for the on state will turn
# on or off in unison.
#
# See: PDF1.7 s12.7.4.2
class ButtonField < Field
define_field :Opt, type: PDFArray, version: '1.4'
# All inheritable dictionary fields for button fields.
INHERITABLE_FIELDS = (superclass::INHERITABLE_FIELDS + [:Opt]).freeze
# Updated list of field flags.
FLAGS_BIT_MAPPING = superclass::FLAGS_BIT_MAPPING.merge(
{
no_toggle_to_off: 14,
radio: 15,
push_button: 16,
radios_in_unison: 25,
}
).freeze
# Initializes the button field to be a push button.
#
# This method should only be called directly after creating a new button field because it
# doesn't completely reset the object.
def initialize_as_push_button
self[:V] = nil
flag(:push_button)
unflag(:radio)
end
# Initializes the button field to be a check box.
#
# This method should only be called directly after creating a new button field because it
# doesn't completely reset the object.
def initialize_as_check_box
self[:V] = :Off
unflag(:push_button)
unflag(:radio)
end
# Initializes the button field to be a radio button.
#
# This method should only be called directly after creating a new button field because it
# doesn't completely reset the object.
def initialize_as_radio_button
self[:V] = :Off
unflag(:push_button)
flag(:radio)
end
# Returns +true+ if this button field represents a push button.
def push_button?
flagged?(:push_button)
end
# Returns +true+ if this button field represents a check box.
def check_box?
!push_button? && !flagged?(:radio)
end
# Returns +true+ if this button field represents a radio button set.
def radio_button?
!push_button? && flagged?(:radio)
end
# Returns the field value which depends on the concrete type.
#
# Push buttons:: They don't have a value, so +nil+ is always returned.
#
# Check boxes:: For check boxes that are in the on state the value +true+ is returned.
# Otherwise +false+ is returned.
#
# Radio buttons:: If no radio button is selected, +nil+ is returned. Otherwise the value (a
# Symbol) of the specific radio button that is selected is returned.
def field_value
normalized_field_value(:V)
end
# Sets the field value which depends on the concrete type.
#
# Push buttons:: Since push buttons don't store any value, the given value is ignored and
# nothing is stored for them (e.g a no-op).
#
# Check boxes:: Use +true+ for checking the box, i.e. toggling it to the on state, and
# +false+ for unchecking it.
#
# Radio buttons:: To turn all radio buttons off, provide +nil+ as value. Otherwise provide
# the value (a Symbol or an object responding to +#to_sym+) of a radio
# button that should be turned on.
def field_value=(value)
normalized_field_value_set(:V, value)
end
# Returns the default field value.
#
# See: #field_value
def default_field_value
normalized_field_value(:DV)
end
# Sets the default field value.
#
# See: #field_value=
def default_field_value=(value)
normalized_field_value_set(:DV, value)
end
# Returns the concrete button field type, either :push_button, :check_box or :radio_button.
def concrete_field_type
if push_button?
:push_button
elsif radio_button?
:radio_button
else
:check_box
end
end
# Returns the name (a Symbol) used for setting the check box to the on state.
#
# Defaults to :Yes if no other name could be determined.
def check_box_on_name
each_widget.to_a.first&.appearance&.normal_appearance&.value&.each_key&.
find {|key| key != :Off } || :Yes
end
# Returns the array of Symbol values that can be used for the field value of the radio
# button.
def radio_button_values
each_widget.map do |widget|
widget.appearance&.normal_appearance&.value&.each_key&.find {|key| key != :Off }
end.compact
end
# Creates a widget for the button field.
#
# If +defaults+ is +true+, then default values will be set on the widget so that it uses a
# default appearance.
#
# If the widget is created for a radio button field, the +value+ argument needs to set to
# the value (a Symbol or an object responding to +#to_sym+) this widget represents. It can
# be used with #field_value= to set this specific widget of the radio button set to on.
#
# See: Field#create_widget, AppearanceGenerator button field methods
def create_widget(page, defaults: true, value: nil, **values)
super(page, allow_embedded: !radio_button?, **values).tap do |widget|
if check_box?
widget[:AP] = {N: {Yes: nil, Off: nil}}
elsif radio_button?
unless value.respond_to?(:to_sym)
raise ArgumentError, "Argument 'value' has to be provided for radio buttons " \
"and needs to respond to #to_sym"
end
widget[:AP] = {N: {value.to_sym => nil, Off: nil}}
end
next unless defaults
widget.border_style(color: 0, width: 1, style: (push_button? ? :beveled : :solid))
widget.background_color(push_button? ? 0.5 : 255)
widget.marker_style(style: check_box? ? :check : :circle) unless push_button?
end
end
# Creates appropriate appearances for all widgets if they don't already exist.
#
# The created appearance streams depend on the actual type of the button field. See
# AppearanceGenerator for the details.
#
# By setting +force+ to +true+ the creation of the appearances can be forced.
def create_appearances(force: false)
appearance_generator_class = document.config.constantize('acro_form.appearance_generator')
each_widget do |widget|
next if !force && widget.appearance?
if check_box?
appearance_generator_class.new(widget).create_check_box_appearances
elsif radio_button?
appearance_generator_class.new(widget).create_radio_button_appearances
else
raise HexaPDF::Error, "Push buttons not yet supported"
end
end
end
# Updates the widgets so that they reflect the current field value.
def update_widgets
return if push_button?
create_appearances
value = self[:V]
each_widget do |widget|
widget[:AS] = (widget.appearance&.normal_appearance&.value&.key?(value) ? value : :Off)
end
end
private
# Returns the normalized field value for the given key which can be :V or :DV.
#
# See #field_value for details.
def normalized_field_value(key)
if push_button?
nil
elsif check_box?
self[key] == check_box_on_name
elsif radio_button?
self[key] == :Off ? nil : self[key]
end
end
# Sets the key, either :V or :DV, to the value. The given normalized value is first
# transformed into the expected value depending on the specific field type.
#
# See #field_value= for details.
def normalized_field_value_set(key, value)
return if push_button?
self[key] = if check_box?
value == true ? check_box_on_name : :Off
elsif value.nil?
:Off
elsif radio_button_values.include?(value.to_sym)
value.to_sym
else
@document.config['acro_form.on_invalid_value'].call(self, value)
end
update_widgets
end
def perform_validation #:nodoc:
if field_type != :Btn
yield("Field /FT of AcroForm button field has to be :Btn", true)
self[:FT] = :Btn
end
super
unless key?(:V)
yield("Button field has no value set, defaulting to :Off", true)
self[:V] = :Off
end
end
end
end
end
end