# -*- 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-2023 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. # # Radio buttons are widgets of a single radio button field. This is also called a radio button # group. Of the radio button group only one radio button (= widget of the radio button field) # may be selected at all times. Each widget must have a different value to be distinguishable; # otherwise the widgets with the same value represent the same thing. Although there is the # +no_toggle_to_off+ field flag, no PDF viewer implements that; one needs to use check boxes # for this feature. # # Check boxes can be toggled on and off. One check box field may have multiple widgets. If # those widgets have the same value, they will all be toggled on or off simultaneously. # Otherwise only one of those widgets will be toggled on while the others are off. In such a # case the check box fields acts like a radio button group, with the additional feature that # no check box may be selected. # # == 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. # # Note: This deselectiong is not implemented in *any* tested PDF viewer. A # work-around is to use multiple check box widgets with different on # names. # # :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: PDF2.0 s12.7.4.2 class ButtonField < Field define_type :XXAcroFormField 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 checked the value of the specific check box that is # checked is returned. Otherwise +nil+ 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:: Provide +nil+ or +false+ as value to toggle all check box widgets off. If # there is only one possible value, +true+ may be used for checking the box, # i.e. toggling it to the on state. Otherwise provide the value (a Symbol or # an object responding to +#to_sym+) of the check box widget that should be # toggled on. # # 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 array of Symbol values (minus the /Off value) that can be used for the field # value for check boxes or radio buttons. # # Note that this will only return useful values if there is at least one correctly set-up # widget. def allowed_values (each_widget.with_object([]) do |widget, result| keys = widget.appearance_dict&.normal_appearance&.value&.keys result.concat(keys) if keys end - [:Off]).uniq 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. # # The +value+ is optional for check box fields; if not specified, the default of :Yes will # be used. # # 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| value = :Yes if check_box? && value.nil? if radio_button? || check_box? 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| normal_appearance = widget.appearance_dict&.normal_appearance next if !force && normal_appearance && ((!push_button? && normal_appearance.value.length == 2 && normal_appearance.each.all? {|_, v| v.kind_of?(HexaPDF::Stream) }) || (push_button? && normal_appearance.kind_of?(HexaPDF::Stream))) 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 appearance_generator_class.new(widget).create_push_button_appearances 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_dict&.normal_appearance&.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 else 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? av = allowed_values self[key] = if value.nil? || value == :Off :Off elsif check_box? if value == false :Off elsif value == true && av.size == 1 av[0] elsif av.include?(value.to_sym) value.to_sym else @document.config['acro_form.on_invalid_value'].call(self, value) end elsif av.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