Class: HexaPDF::Type::AcroForm::AppearanceGenerator

Inherits:
Object
  • Object
show all
Defined in:
lib/hexapdf/type/acro_form/appearance_generator.rb

Overview

The AppearanceGenerator class provides methods for generating and updating the appearance streams of form fields.

The only method needed is #create_appearances since this method determines to what field the widget belongs and therefore which appearance should be generated.

The visual appearance of a field is constructed using information from the field itself as well as information from the widget. See the documentation for the individual methods which information is used in which way.

By default, any existing appearances are overwritten and the :print flag is set on the widget so that the field appearance will appear on print-outs.

The visual appearances are chosen to be similar to those used by Adobe Acrobat and others. By subclassing and overriding the necessary methods it is possible to define custom appearances.

See: PDF2.0 s12.5.5, s12.7

Instance Method Summary collapse

Constructor Details

#initialize(widget) ⇒ AppearanceGenerator

Creates a new instance for the given widget.



69
70
71
72
73
# File 'lib/hexapdf/type/acro_form/appearance_generator.rb', line 69

def initialize(widget)
  @widget = widget
  @field = widget.form_field
  @document = widget.document
end

Instance Method Details

#create_appearancesObject

Creates the appropriate appearances for the widget.



76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/hexapdf/type/acro_form/appearance_generator.rb', line 76

def create_appearances
  case @field.field_type
  when :Btn
    if @field.push_button?
      create_push_button_appearances
    else
      create_check_box_appearances
    end
  when :Tx, :Ch
    create_text_appearances
  else
    raise HexaPDF::Error, "Unsupported field type #{@field.field_type}"
  end
end

#create_check_box_appearancesObject Also known as: create_radio_button_appearances

Creates the appropriate appearances for check boxes and radio buttons.

The unchecked box or unselected radio button is always represented by the appearance with the key /Off. If there is more than one other key besides the /Off key, the first one is used for the appearance of the checked box or selected radio button.

For unchecked boxes an empty rectangle is drawn. Similarly, for unselected radio buttons an empty circle (if the marker is :circle) or rectangle is drawn. When checked or selected, a symbol from the ZapfDingbats font is placed inside. How this is exactly done depends on the following values:

  • The widget’s rectangle /Rect must be defined. If the height and/or width of the rectangle are zero, they are based on the configuration option acro_form.default_font_size and widget’s border width. In such a case the rectangle is appropriately updated.

  • The line width, style and color of the cirle/rectangle are taken from the widget’s border style. See HexaPDF::Type::Annotations::Widget#border_style.

  • The background color is determined by the widget’s background color. See HexaPDF::Type::Annotations::Widget#background_color.

  • The symbol (marker) as well as its size and color are determined by the marker style of the widget. See HexaPDF::Type::Annotations::Widget#marker_style for details.

Examples:

# check box: default appearance
widget.border_style(color: 0)
widget.background_color(1)
widget.marker_style(style: :check, size: 0, color: 0)

# check box: no visible rectangle, gray background, cross mark when checked
widget.border_style(color: :transparent, width: 2)
widget.background_color(0.7)
widget.marker_style(style: :cross)

# radio button: default appearance
widget.border_style(color: 0)
widget.background_color(1)
widget.marker_style(style: :circle, size: 0, color: 0)


132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/hexapdf/type/acro_form/appearance_generator.rb', line 132

def create_check_box_appearances
  normal_appearance = @widget.appearance_dict&.normal_appearance
  if !normal_appearance.kind_of?(HexaPDF::Dictionary) || normal_appearance.kind_of?(HexaPDF::Stream)
    (@widget[:AP] ||= {})[:N] = {Off: nil}
    normal_appearance = @widget[:AP][:N]
    normal_appearance[@field.field_value&.to_sym || :Yes] = nil
  end
  on_name = (normal_appearance.value.keys - [:Off]).first
  unless on_name
    on_name = @field.field_value&.to_sym || :Yes
    normal_appearance[on_name] = nil
  end

  @widget[:AS] = (@field[:V] == on_name ? on_name : :Off)
  @widget.flag(:print)
  @widget.unflag(:hidden)

  border_style = @widget.border_style
  marker_style = @widget.marker_style
  circular = @field.radio_button? && marker_style.style == :circle

  default_font_size = @document.config['acro_form.default_font_size']
  rect = @widget[:Rect]
  rect.width = default_font_size + 2 * border_style.width if rect.width == 0
  rect.height = default_font_size + 2 * border_style.width if rect.height == 0

  width, height, matrix = perform_rotation(rect.width, rect.height)

  off_form = @widget.appearance_dict.normal_appearance[:Off] =
    @document.add({Type: :XObject, Subtype: :Form, BBox: [0, 0, width, height],
                   Matrix: matrix})
  apply_background_and_border(border_style, off_form.canvas, circular: circular)

  on_form = @widget.appearance_dict.normal_appearance[on_name] =
    @document.add({Type: :XObject, Subtype: :Form, BBox: [0, 0, width, height],
                   Matrix: matrix})
  canvas = on_form.canvas
  apply_background_and_border(border_style, canvas, circular: circular)
  canvas.save_graphics_state do
    draw_marker(canvas, width, height, border_style.width, marker_style)
  end
end

#create_push_button_appearancesObject

Creates the appropriate appearances for push buttons.

This is currently a dummy implementation raising an error.

Raises:



180
181
182
# File 'lib/hexapdf/type/acro_form/appearance_generator.rb', line 180

def create_push_button_appearances
  raise HexaPDF::Error, "Push button appearance generation not yet supported"
end

#create_text_appearancesObject Also known as: create_combo_box_appearances, create_list_box_appearances

Creates the appropriate appearances for text fields, combo box fields and list box fields.

The following describes how the appearance is built:

  • The font, font size and font color are taken from the associated field’s default appearance string. See VariableTextField.

    If the font is not usable by HexaPDF (which may be due to a variety of reasons, e.g. no associated information in the form’s default resources), the font specified by the configuration option acro_form.fallback_font will be used.

  • The widget’s rectangle /Rect must be defined. If the height is zero, it is auto-sized based on the font size. If additionally the font size is zero, a font size of acro_form.default_font_size is used. If the width is zero, the acro_form.text_field.default_width value is used. In such cases the rectangle is appropriately updated.

  • The line width, style and color of the rectangle are taken from the widget’s border style. See HexaPDF::Type::Annotations::Widget#border_style.

  • The background color is determined by the widget’s background color. See HexaPDF::Type::Annotations::Widget#background_color.

Note: Rich text fields are currently not supported!



208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
# File 'lib/hexapdf/type/acro_form/appearance_generator.rb', line 208

def create_text_appearances
  default_resources = @document.acro_form.default_resources
  font, font_size, font_color = retrieve_font_information(default_resources)
  style = HexaPDF::Layout::Style.new(font: font, font_size: font_size, fill_color: font_color)
  border_style = @widget.border_style
  padding = [1, border_style.width].max

  @widget[:AS] = :N
  @widget.flag(:print)
  @widget.unflag(:hidden)
  rect = @widget[:Rect]
  rect.width = @document.config['acro_form.text_field.default_width'] if rect.width == 0
  if rect.height == 0
    style.font_size =
      (font_size == 0 ? @document.config['acro_form.default_font_size'] : font_size)
    rect.height = style.scaled_y_max - style.scaled_y_min + 2 * padding
  end

  width, height, matrix = perform_rotation(rect.width, rect.height)

  form = (@widget[:AP] ||= {})[:N] ||= @document.add({Type: :XObject, Subtype: :Form})
  # Wrap existing object in Form class in case the PDF writer didn't include the /Subtype
  # key or the type of the object is wrong; we can do this since we know this has to be a
  # Form object
  unless form.type == :XObject && form[:Subtype] == :Form
    form = @document.wrap(form, type: :XObject, subtype: :Form)
  end
  form.value.replace({Type: :XObject, Subtype: :Form, BBox: [0, 0, width, height],
                      Matrix: matrix, Resources: HexaPDF::Object.deep_copy(default_resources)})
  form.contents = ''

  canvas = form.canvas
  apply_background_and_border(border_style, canvas)

  canvas.marked_content_sequence(:Tx) do
    if @field.field_value || @field.concrete_field_type == :list_box
      canvas.save_graphics_state do
        canvas.rectangle(padding, padding, width - 2 * padding,
                         height - 2 * padding).clip_path.end_path
        case @field.concrete_field_type
        when :multiline_text_field
          draw_multiline_text(canvas, width, height, style, padding)
        when :list_box
          draw_list_box(canvas, width, height, style, padding)
        else
          draw_single_line_text(canvas, width, height, style, padding)
        end
      end
    end
  end
end