hexapdf 0.12.3 → 0.14.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +132 -0
  3. data/examples/019-acro_form.rb +41 -4
  4. data/lib/hexapdf/cli/command.rb +4 -2
  5. data/lib/hexapdf/cli/image2pdf.rb +2 -1
  6. data/lib/hexapdf/cli/info.rb +51 -2
  7. data/lib/hexapdf/cli/inspect.rb +30 -8
  8. data/lib/hexapdf/cli/merge.rb +1 -1
  9. data/lib/hexapdf/cli/split.rb +74 -14
  10. data/lib/hexapdf/configuration.rb +15 -0
  11. data/lib/hexapdf/content/graphic_object/arc.rb +3 -3
  12. data/lib/hexapdf/dictionary.rb +12 -6
  13. data/lib/hexapdf/dictionary_fields.rb +2 -10
  14. data/lib/hexapdf/document.rb +41 -16
  15. data/lib/hexapdf/document/files.rb +0 -1
  16. data/lib/hexapdf/encryption/fast_arc4.rb +1 -1
  17. data/lib/hexapdf/encryption/security_handler.rb +1 -0
  18. data/lib/hexapdf/encryption/standard_security_handler.rb +1 -0
  19. data/lib/hexapdf/font/cmap.rb +1 -4
  20. data/lib/hexapdf/font/true_type/subsetter.rb +16 -3
  21. data/lib/hexapdf/font/true_type/table/head.rb +1 -0
  22. data/lib/hexapdf/font/true_type/table/os2.rb +2 -0
  23. data/lib/hexapdf/font/true_type/table/post.rb +15 -10
  24. data/lib/hexapdf/font_loader/from_configuration.rb +2 -2
  25. data/lib/hexapdf/font_loader/from_file.rb +18 -8
  26. data/lib/hexapdf/image_loader/png.rb +3 -2
  27. data/lib/hexapdf/importer.rb +3 -2
  28. data/lib/hexapdf/layout/line.rb +1 -1
  29. data/lib/hexapdf/layout/style.rb +23 -23
  30. data/lib/hexapdf/layout/text_layouter.rb +2 -2
  31. data/lib/hexapdf/layout/text_shaper.rb +3 -2
  32. data/lib/hexapdf/object.rb +52 -25
  33. data/lib/hexapdf/parser.rb +107 -7
  34. data/lib/hexapdf/pdf_array.rb +15 -5
  35. data/lib/hexapdf/revisions.rb +29 -21
  36. data/lib/hexapdf/serializer.rb +37 -10
  37. data/lib/hexapdf/task/optimize.rb +6 -4
  38. data/lib/hexapdf/tokenizer.rb +22 -0
  39. data/lib/hexapdf/type/acro_form/appearance_generator.rb +130 -27
  40. data/lib/hexapdf/type/acro_form/button_field.rb +5 -2
  41. data/lib/hexapdf/type/acro_form/choice_field.rb +68 -14
  42. data/lib/hexapdf/type/acro_form/field.rb +35 -5
  43. data/lib/hexapdf/type/acro_form/form.rb +139 -14
  44. data/lib/hexapdf/type/acro_form/text_field.rb +70 -4
  45. data/lib/hexapdf/type/actions/uri.rb +3 -2
  46. data/lib/hexapdf/type/annotations/widget.rb +3 -4
  47. data/lib/hexapdf/type/catalog.rb +2 -2
  48. data/lib/hexapdf/type/cid_font.rb +1 -1
  49. data/lib/hexapdf/type/file_specification.rb +1 -1
  50. data/lib/hexapdf/type/font.rb +1 -1
  51. data/lib/hexapdf/type/font_simple.rb +4 -2
  52. data/lib/hexapdf/type/font_true_type.rb +6 -2
  53. data/lib/hexapdf/type/font_type0.rb +4 -4
  54. data/lib/hexapdf/type/form.rb +6 -2
  55. data/lib/hexapdf/type/image.rb +2 -2
  56. data/lib/hexapdf/type/page.rb +21 -12
  57. data/lib/hexapdf/type/page_tree_node.rb +29 -5
  58. data/lib/hexapdf/type/resources.rb +5 -0
  59. data/lib/hexapdf/type/trailer.rb +2 -3
  60. data/lib/hexapdf/utils/object_hash.rb +0 -1
  61. data/lib/hexapdf/utils/sorted_tree_node.rb +18 -15
  62. data/lib/hexapdf/version.rb +1 -1
  63. data/test/hexapdf/common_tokenizer_tests.rb +2 -2
  64. data/test/hexapdf/content/graphic_object/test_arc.rb +4 -4
  65. data/test/hexapdf/content/test_canvas.rb +3 -3
  66. data/test/hexapdf/content/test_color_space.rb +1 -1
  67. data/test/hexapdf/encryption/test_aes.rb +4 -4
  68. data/test/hexapdf/encryption/test_standard_security_handler.rb +11 -11
  69. data/test/hexapdf/filter/test_ascii85_decode.rb +1 -1
  70. data/test/hexapdf/filter/test_ascii_hex_decode.rb +1 -1
  71. data/test/hexapdf/font/true_type/table/test_post.rb +1 -1
  72. data/test/hexapdf/font/true_type/test_subsetter.rb +10 -0
  73. data/test/hexapdf/font_loader/test_from_configuration.rb +7 -3
  74. data/test/hexapdf/font_loader/test_from_file.rb +7 -0
  75. data/test/hexapdf/layout/test_text_layouter.rb +12 -5
  76. data/test/hexapdf/test_configuration.rb +2 -2
  77. data/test/hexapdf/test_dictionary.rb +8 -1
  78. data/test/hexapdf/test_dictionary_fields.rb +9 -2
  79. data/test/hexapdf/test_document.rb +18 -10
  80. data/test/hexapdf/test_object.rb +71 -26
  81. data/test/hexapdf/test_parser.rb +205 -51
  82. data/test/hexapdf/test_pdf_array.rb +8 -1
  83. data/test/hexapdf/test_revisions.rb +35 -0
  84. data/test/hexapdf/test_serializer.rb +7 -0
  85. data/test/hexapdf/test_tokenizer.rb +28 -0
  86. data/test/hexapdf/test_writer.rb +2 -2
  87. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +288 -35
  88. data/test/hexapdf/type/acro_form/test_button_field.rb +15 -0
  89. data/test/hexapdf/type/acro_form/test_choice_field.rb +92 -9
  90. data/test/hexapdf/type/acro_form/test_field.rb +39 -0
  91. data/test/hexapdf/type/acro_form/test_form.rb +87 -15
  92. data/test/hexapdf/type/acro_form/test_text_field.rb +77 -1
  93. data/test/hexapdf/type/test_font_simple.rb +2 -1
  94. data/test/hexapdf/type/test_font_true_type.rb +6 -0
  95. data/test/hexapdf/type/test_form.rb +8 -1
  96. data/test/hexapdf/type/test_page.rb +8 -1
  97. data/test/hexapdf/type/test_page_tree_node.rb +42 -0
  98. data/test/hexapdf/type/test_resources.rb +6 -0
  99. data/test/hexapdf/utils/test_bit_field.rb +2 -0
  100. data/test/hexapdf/utils/test_object_hash.rb +5 -0
  101. data/test/hexapdf/utils/test_sorted_tree_node.rb +10 -9
  102. data/test/test_helper.rb +2 -0
  103. metadata +6 -12
@@ -228,10 +228,12 @@ module HexaPDF
228
228
  #
229
229
  # The created appearance streams depend on the actual type of the button field. See
230
230
  # AppearanceGenerator for the details.
231
- def create_appearances
231
+ #
232
+ # By setting +force+ to +true+ the creation of the appearances can be forced.
233
+ def create_appearances(force: false)
232
234
  appearance_generator_class = document.config.constantize('acro_form.appearance_generator')
233
235
  each_widget do |widget|
234
- next if widget.appearance?
236
+ next if !force && widget.appearance?
235
237
  if check_box?
236
238
  appearance_generator_class.new(widget).create_check_box_appearances
237
239
  elsif radio_button?
@@ -245,6 +247,7 @@ module HexaPDF
245
247
  # Updates the widgets so that they reflect the current field value.
246
248
  def update_widgets
247
249
  return if push_button?
250
+ create_appearances
248
251
  value = self[:V]
249
252
  each_widget do |widget|
250
253
  widget[:AS] = (widget.appearance&.normal_appearance&.value&.key?(value) ? value : :Off)
@@ -122,17 +122,24 @@ module HexaPDF
122
122
  end
123
123
 
124
124
  # Sets the field value to the given string or array of strings.
125
+ #
126
+ # The dictionary field /I is also modified to correctly represent the selected item(s).
125
127
  def field_value=(value)
126
128
  items = option_items
127
- all_included = [value].flatten.all? {|v| items.include?(v) }
129
+ array_value = [value].flatten
130
+ all_included = array_value.all? {|v| items.include?(v) }
128
131
  self[:V] = if (combo_box? && value.kind_of?(String) &&
129
- (flagged?(:edit) || all_included)) ||
130
- (list_box? && all_included &&
131
- (value.kind_of?(String) || flagged?(:multi_select)))
132
+ (flagged?(:edit) || all_included))
133
+ delete(:I)
132
134
  value
135
+ elsif list_box? && all_included &&
136
+ (value.kind_of?(String) || flagged?(:multi_select))
137
+ self[:I] = array_value.map {|val| items.index(val) }.sort!
138
+ array_value.length == 1 ? value : array_value
133
139
  else
134
140
  @document.config['acro_form.on_invalid_value'].call(self, value)
135
141
  end
142
+ update_widgets
136
143
  end
137
144
 
138
145
  # Returns the default field value.
@@ -148,20 +155,54 @@ module HexaPDF
148
155
  def default_field_value=(value)
149
156
  items = option_items
150
157
  self[:DV] = if [value].flatten.all? {|v| items.include?(v) }
151
- value
152
- else
153
- @document.config['acro_form.on_invalid_value'].call(self, value)
154
- end
158
+ value
159
+ else
160
+ @document.config['acro_form.on_invalid_value'].call(self, value)
161
+ end
155
162
  end
156
163
 
157
164
  # Returns the array with the available option items.
165
+ #
166
+ # Note that this *only* returns the option items themselves! For getting the export values,
167
+ # the #export_values method has to be used.
158
168
  def option_items
159
- key?(:Opt) ? process_value(self[:Opt]) : self[:Opt] ||= []
169
+ key?(:Opt) ? process_value(self[:Opt].map {|i| i.kind_of?(Array) ? i[1] : i }) : []
170
+ end
171
+
172
+ # Returns the export values of the option items.
173
+ #
174
+ # If you need the display strings (as in most cases), use the #option_items method.
175
+ def export_values
176
+ key?(:Opt) ? process_value(self[:Opt].map {|i| i.kind_of?(Array) ? i[0] : i }) : []
160
177
  end
161
178
 
162
179
  # Sets the array with the available option items to the given value.
180
+ #
181
+ # Each entry in the array may either be a string representing the text to be displayed. Or
182
+ # an array of two strings where the first describes the export value (to be used when
183
+ # exporting form field data from the document) and the second is the display value.
184
+ #
185
+ # See: #option_items, #export_values
163
186
  def option_items=(value)
164
- self[:Opt] = value
187
+ self[:Opt] = if flagged?(:sort)
188
+ value.sort_by {|i| process_value(i.kind_of?(Array) ? i[1] : i) }
189
+ else
190
+ value
191
+ end
192
+ end
193
+
194
+ # Returns the index of the first visible option item of a list box.
195
+ def list_box_top_index
196
+ self[:TI]
197
+ end
198
+
199
+ # Makes the option item referred to via the given +index+ the first visible option item of a
200
+ # list box.
201
+ def list_box_top_index=(index)
202
+ if index < 0 || !key?(:Opt) || index >= self[:Opt].length
203
+ raise ArgumentError, "Index out of range for the set option items"
204
+ end
205
+ self[:TI] = index
165
206
  end
166
207
 
167
208
  # Returns the concrete choice field type, either :list_box, :combo_box or
@@ -178,19 +219,32 @@ module HexaPDF
178
219
  #
179
220
  # For information on how this is done see AppearanceGenerator.
180
221
  #
181
- # Note that an appearance for a choice field widget is *always* created even if there is an
182
- # existing one to make sure the current field value is properly represented.
183
- def create_appearances
222
+ # Note that no new appearances are created if the dictionary fields involved in the creation
223
+ # of the appearance stream have not been changed between invocations.
224
+ #
225
+ # By setting +force+ to +true+ the creation of the appearances can be forced.
226
+ def create_appearances(force: false)
227
+ current_appearance_state = [self[:V], self[:I], self[:Opt], self[:TI]]
228
+
184
229
  appearance_generator_class = document.config.constantize('acro_form.appearance_generator')
185
230
  each_widget do |widget|
231
+ next if !force && widget.cached?(:appearance_state) &&
232
+ widget.cache(:appearance_state) == current_appearance_state
233
+
234
+ widget.cache(:appearance_state, current_appearance_state, update: true)
186
235
  if combo_box?
187
236
  appearance_generator_class.new(widget).create_combo_box_appearances
188
237
  else
189
- raise HexaPDF::Error, "List boxes not yet supported"
238
+ appearance_generator_class.new(widget).create_list_box_appearances
190
239
  end
191
240
  end
192
241
  end
193
242
 
243
+ # Updates the widgets so that they reflect the current field value.
244
+ def update_widgets
245
+ create_appearances
246
+ end
247
+
194
248
  private
195
249
 
196
250
  # Uses the HexaPDF::DictionaryFields::StringConverter to process the value (a string or an
@@ -236,6 +236,11 @@ module HexaPDF
236
236
  kids.nil? || kids.empty? || kids.none? {|kid| kid.key?(:T) }
237
237
  end
238
238
 
239
+ # Returns +true+ if the field contains an embedded widget.
240
+ def embedded_widget?
241
+ key?(:Subtype)
242
+ end
243
+
239
244
  # :call-seq:
240
245
  # field.each_widget {|widget| block} -> field
241
246
  # field.each_widget -> Enumerator
@@ -245,7 +250,7 @@ module HexaPDF
245
250
  # See: HexaPDF::Type::Annotations::Widget
246
251
  def each_widget # :yields: widget
247
252
  return to_enum(__method__) unless block_given?
248
- if self[:Subtype]
253
+ if embedded_widget?
249
254
  yield(document.wrap(self))
250
255
  elsif terminal_field?
251
256
  self[:Kids]&.each {|kid| yield(document.wrap(kid)) }
@@ -275,9 +280,9 @@ module HexaPDF
275
280
 
276
281
  widget_data = {Type: :Annot, Subtype: :Widget, Rect: [0, 0, 0, 0], **values}
277
282
 
278
- if !allow_embedded || key?(:Subtype) || (key?(:Kids) && !self[:Kids].empty?)
283
+ if !allow_embedded || embedded_widget? || (key?(:Kids) && !self[:Kids].empty?)
279
284
  kids = self[:Kids] ||= []
280
- kids << extract_widget if key?(:Subtype)
285
+ kids << extract_widget if embedded_widget?
281
286
  widget = document.add(widget_data)
282
287
  widget[:Parent] = self
283
288
  self[:Kids] << widget
@@ -291,6 +296,31 @@ module HexaPDF
291
296
  widget
292
297
  end
293
298
 
299
+ # Deletes the given widget annotation object from this field, the page it appears on and the
300
+ # document.
301
+ #
302
+ # If the given widget is not a widget of this field, nothing is done.
303
+ def delete_widget(widget)
304
+ widget = if embedded_widget? && self == widget
305
+ widget
306
+ elsif terminal_field?
307
+ (widget_index = self[:Kids]&.index(widget)) && widget
308
+ end
309
+
310
+ return unless widget
311
+
312
+ document.pages.each do |page|
313
+ break if page[:Annots]&.delete(widget) # See comment in #extract_widget
314
+ end
315
+
316
+ if embedded_widget?
317
+ WIDGET_FIELDS.each {|key| delete(key) }
318
+ else
319
+ self[:Kids].delete_at(widget_index)
320
+ document.delete(widget)
321
+ end
322
+ end
323
+
294
324
  private
295
325
 
296
326
  # An array of all widget annotation field names.
@@ -300,14 +330,14 @@ module HexaPDF
300
330
  # directly in the field and adjust the references accordingly. If the field doesn't have any
301
331
  # widget data, +nil+ is returned.
302
332
  def extract_widget
303
- return unless key?(:Subtype)
333
+ return unless embedded_widget?
304
334
  data = WIDGET_FIELDS.each_with_object({}) do |key, hash|
305
335
  hash[key] = delete(key) if key?(key)
306
336
  end
307
337
  widget = document.add(data, type: :Annot)
308
338
  widget[:Parent] = self
309
339
  document.pages.each do |page|
310
- if page.key?(:Annots) && (index = page[:Annots].index {|annot| annot.data == self.data })
340
+ if page.key?(:Annots) && (index = page[:Annots].index(self))
311
341
  page[:Annots][index] = widget
312
342
  break # Each annotation dictionary may only appear on one page, see PDF1.7 12.5.2
313
343
  end
@@ -153,8 +153,83 @@ module HexaPDF
153
153
  #
154
154
  # The +name+ may contain dots to signify a field hierarchy. If so, the referenced parent
155
155
  # fields must already exist. If it doesn't contain dots, a top-level field is created.
156
- def create_text_field(name)
157
- create_field(name, :Tx)
156
+ #
157
+ # The optional keyword arguments allow setting often used properties of the field:
158
+ #
159
+ # +font+::
160
+ # The font that should be used for the text of the field. If +font_size+ is specified
161
+ # but +font+ isn't, the font Helvetica is used.
162
+ #
163
+ # +font_size+::
164
+ # The font size that should be used. If +font+ is specified but +font_size+ isn't, font
165
+ # size defaults to 0 (= auto-sizing).
166
+ #
167
+ # +align+::
168
+ # The alignment of the text, either :left, :center or :right.
169
+ def create_text_field(name, font: nil, font_size: nil, align: nil)
170
+ create_field(name, :Tx) do |field|
171
+ apply_variable_text_properties(field, font: font, font_size: font_size, align: align)
172
+ end
173
+ end
174
+
175
+ # Creates a new multiline text field with the given name and adds it to the form.
176
+ #
177
+ # The +name+ may contain dots to signify a field hierarchy. If so, the referenced parent
178
+ # fields must already exist. If it doesn't contain dots, a top-level field is created.
179
+ #
180
+ # The optional keyword arguments allow setting often used properties of the field, see
181
+ # #create_text_field for details.
182
+ def create_multiline_text_field(name, font: nil, font_size: nil, align: nil)
183
+ create_field(name, :Tx) do |field|
184
+ field.initialize_as_multiline_text_field
185
+ apply_variable_text_properties(field, font: font, font_size: font_size, align: align)
186
+ end
187
+ end
188
+
189
+ # Creates a new comb text field with the given name and adds it to the form.
190
+ #
191
+ # The +max_chars+ argument defines the maximum number of characters the comb text field can
192
+ # accommodate.
193
+ #
194
+ # The +name+ may contain dots to signify a field hierarchy. If so, the referenced parent
195
+ # fields must already exist. If it doesn't contain dots, a top-level field is created.
196
+ #
197
+ # The optional keyword arguments allow setting often used properties of the field, see
198
+ # #create_text_field for details.
199
+ def create_comb_text_field(name, max_chars:, font: nil, font_size: nil, align: nil)
200
+ create_field(name, :Tx) do |field|
201
+ field.initialize_as_comb_text_field
202
+ apply_variable_text_properties(field, font: font, font_size: font_size, align: align)
203
+ field[:MaxLen] = max_chars
204
+ end
205
+ end
206
+
207
+ # Creates a new file select field with the given name and adds it to the form.
208
+ #
209
+ # The +name+ may contain dots to signify a field hierarchy. If so, the referenced parent
210
+ # fields must already exist. If it doesn't contain dots, a top-level field is created.
211
+ #
212
+ # The optional keyword arguments allow setting often used properties of the field, see
213
+ # #create_text_field for details.
214
+ def create_file_select_field(name, font: nil, font_size: nil, align: nil)
215
+ create_field(name, :Tx) do |field|
216
+ field.initialize_as_file_select_field
217
+ apply_variable_text_properties(field, font: font, font_size: font_size, align: align)
218
+ end
219
+ end
220
+
221
+ # Creates a new password field with the given name and adds it to the form.
222
+ #
223
+ # The +name+ may contain dots to signify a field hierarchy. If so, the referenced parent
224
+ # fields must already exist. If it doesn't contain dots, a top-level field is created.
225
+ #
226
+ # The optional keyword arguments allow setting often used properties of the field, see
227
+ # #create_text_field for details.
228
+ def create_password_field(name, font: nil, font_size: nil, align: nil)
229
+ create_field(name, :Tx) do |field|
230
+ field.initialize_as_password_field
231
+ apply_variable_text_properties(field, font: font, font_size: font_size, align: align)
232
+ end
158
233
  end
159
234
 
160
235
  # Creates a new check box with the given name and adds it to the form.
@@ -162,7 +237,7 @@ module HexaPDF
162
237
  # The +name+ may contain dots to signify a field hierarchy. If so, the referenced parent
163
238
  # fields must already exist. If it doesn't contain dots, a top-level field is created.
164
239
  def create_check_box(name)
165
- create_field(name, :Btn).tap(&:initialize_as_check_box)
240
+ create_field(name, :Btn, &:initialize_as_check_box)
166
241
  end
167
242
 
168
243
  # Creates a radio button with the given name and adds it to the form.
@@ -170,28 +245,64 @@ module HexaPDF
170
245
  # The +name+ may contain dots to signify a field hierarchy. If so, the referenced parent
171
246
  # fields must already exist. If it doesn't contain dots, a top-level field is created.
172
247
  def create_radio_button(name)
173
- create_field(name, :Btn).tap(&:initialize_as_radio_button)
248
+ create_field(name, :Btn, &:initialize_as_radio_button)
174
249
  end
175
250
 
176
251
  # Creates a combo box with the given name and adds it to the form.
177
252
  #
178
253
  # The +name+ may contain dots to signify a field hierarchy. If so, the referenced parent
179
254
  # fields must already exist. If it doesn't contain dots, a top-level field is created.
180
- def create_combo_box(name)
181
- create_field(name, :Ch).tap(&:initialize_as_combo_box)
255
+ #
256
+ # The optional keyword arguments allow setting often used properties of the field:
257
+ #
258
+ # +option_items+::
259
+ # Specifies the values of the list box.
260
+ #
261
+ # +editable+::
262
+ # If set to +true+, the combo box allows entering an arbitrary value in addition to
263
+ # selecting one of the provided option items.
264
+ #
265
+ # +font+, +font_size+ and +align+::
266
+ # See #create_text_field
267
+ def create_combo_box(name, option_items: nil, editable: nil, font: nil, font_size: nil,
268
+ align: nil)
269
+ create_field(name, :Ch) do |field|
270
+ field.initialize_as_combo_box
271
+ field.option_items = option_items if option_items
272
+ field.flag(:edit) if editable
273
+ apply_variable_text_properties(field, font: font, font_size: font_size, align: align)
274
+ end
182
275
  end
183
276
 
184
277
  # Creates a list box with the given name and adds it to the form.
185
278
  #
186
279
  # The +name+ may contain dots to signify a field hierarchy. If so, the referenced parent
187
280
  # fields must already exist. If it doesn't contain dots, a top-level field is created.
188
- def create_list_box(name)
189
- create_field(name, :Ch).tap(&:initialize_as_list_box)
281
+ #
282
+ # The optional keyword arguments allow setting often used properties of the field:
283
+ #
284
+ # +option_items+::
285
+ # Specifies the values of the list box.
286
+ #
287
+ # +multi_select+::
288
+ # If set to +true+, the list box allows selecting multiple items instead of only one.
289
+ #
290
+ # +font+, +font_size+ and +align+::
291
+ # See #create_text_field.
292
+ def create_list_box(name, option_items: nil, multi_select: nil, font: nil, font_size: nil,
293
+ align: nil)
294
+ create_field(name, :Ch) do |field|
295
+ field.initialize_as_list_box
296
+ field.option_items = option_items if option_items
297
+ field.flag(:multi_select) if multi_select
298
+ apply_variable_text_properties(field, font: font, font_size: font_size, align: align)
299
+ end
190
300
  end
191
301
 
192
302
  # Returns the dictionary containing the default resources for form field appearance streams.
193
303
  def default_resources
194
- self[:DR] ||= document.wrap({}, type: :XXResources)
304
+ self[:DR] ||= document.wrap({ProcSet: [:PDF, :Text, :ImageB, :ImageC, :ImageI]},
305
+ type: :XXResources)
195
306
  end
196
307
 
197
308
  # Sets the global default appearance string using the provided values.
@@ -212,9 +323,11 @@ module HexaPDF
212
323
  end
213
324
 
214
325
  # Creates the appearances for all widgets of all terminal fields if they don't exist.
215
- def create_appearances
326
+ #
327
+ # If +force+ is +true+, new appearances are created even if there are existing ones.
328
+ def create_appearances(force: false)
216
329
  each_field do |field|
217
- field.create_appearances if field.respond_to?(:create_appearances)
330
+ field.create_appearances(force: force) if field.respond_to?(:create_appearances)
218
331
  end
219
332
  end
220
333
 
@@ -245,18 +358,30 @@ module HexaPDF
245
358
  else
246
359
  (self[:Fields] ||= []) << field
247
360
  end
361
+
362
+ yield(field)
363
+
248
364
  field
249
365
  end
250
366
 
367
+ # Applies the given variable field properties to the field.
368
+ def apply_variable_text_properties(field, font: nil, font_size: nil, align: nil)
369
+ if font || font_size
370
+ field.set_default_appearance_string(font: font || 'Helvetica', font_size: font_size || 0)
371
+ end
372
+ field.text_alignment(align) if align
373
+ end
374
+
251
375
  def perform_validation # :nodoc:
376
+ super
377
+
252
378
  if (da = self[:DA])
253
379
  unless self[:DR]
254
380
  yield("When the field /DA is present, the field /DR must also be present")
381
+ return
255
382
  end
256
383
  font_name = nil
257
- HexaPDF::Content::Parser.parse(da) do |obj, params|
258
- font_name = params[0] if obj == :Tf
259
- end
384
+ HexaPDF::Content::Parser.parse(da) {|obj, params| font_name = params[0] if obj == :Tf }
260
385
  if font_name && !(self[:DR][:Font] && self[:DR][:Font][font_name])
261
386
  yield("The font specified in /DA is not in the /DR resource dictionary")
262
387
  end
@@ -44,6 +44,9 @@ module HexaPDF
44
44
  # AcroForm text fields provide a box or space to fill-in data entered from keyboard. The text
45
45
  # may be restricted to a single line or can span multiple lines.
46
46
  #
47
+ # A special type of single-line text field is the comb text field. This type of field divides
48
+ # the existing space into /MaxLen equally spaced positions.
49
+ #
47
50
  # == Type Specific Field Flags
48
51
  #
49
52
  # :multiline:: If set, the text field may contain multiple lines.
@@ -88,6 +91,63 @@ module HexaPDF
88
91
  }
89
92
  ).freeze
90
93
 
94
+ # Initializes the text field to be a multiline text field.
95
+ #
96
+ # This method should only be called directly after creating a new text field because it
97
+ # doesn't completely reset the object.
98
+ def initialize_as_multiline_text_field
99
+ flag(:multiline)
100
+ unflag(:file_select, :comb, :password)
101
+ end
102
+
103
+ # Initializes the text field to be a comb text field.
104
+ #
105
+ # This method should only be called directly after creating a new text field because it
106
+ # doesn't completely reset the object.
107
+ def initialize_as_comb_text_field
108
+ flag(:comb)
109
+ unflag(:file_select, :multiline, :password)
110
+ end
111
+
112
+ # Initializes the text field to be a password field.
113
+ #
114
+ # This method should only be called directly after creating a new text field because it
115
+ # doesn't completely reset the object.
116
+ def initialize_as_password_field
117
+ delete(:V)
118
+ flag(:password)
119
+ unflag(:comb, :multiline, :file_select)
120
+ end
121
+
122
+ # Initializes the text field to be a file select field.
123
+ #
124
+ # This method should only be called directly after creating a new text field because it
125
+ # doesn't completely reset the object.
126
+ def initialize_as_file_select_field
127
+ flag(:file_select)
128
+ unflag(:comb, :multiline, :password)
129
+ end
130
+
131
+ # Returns +true+ if this field is a multiline text field.
132
+ def multiline_text_field?
133
+ flagged?(:multiline) && !(flagged?(:file_select) || flagged?(:comb) || flagged?(:password))
134
+ end
135
+
136
+ # Returns +true+ if this field is a comb text field.
137
+ def comb_text_field?
138
+ flagged?(:comb) && !(flagged?(:file_select) || flagged?(:multiline) || flagged?(:password))
139
+ end
140
+
141
+ # Returns +true+ if this field is a password field.
142
+ def password_field?
143
+ flagged?(:password) && !(flagged?(:file_select) || flagged?(:multiline) || flagged?(:comb))
144
+ end
145
+
146
+ # Returns +true+ if this field is a file select field.
147
+ def file_select_field?
148
+ flagged?(:file_select) && !(flagged?(:password) || flagged?(:multiline) || flagged?(:comb))
149
+ end
150
+
91
151
  # Returns the field value, i.e. the text contents of the field, or +nil+ if no value is set.
92
152
  #
93
153
  # Note that modifying the returned value *might not* modify the text contents in case it is
@@ -147,11 +207,16 @@ module HexaPDF
147
207
  #
148
208
  # For information on how this is done see AppearanceGenerator.
149
209
  #
150
- # Note that an appearance for a text field widget is *always* created even if there is an
151
- # existing one to make sure the current field value is properly represented.
152
- def create_appearances
210
+ # Note that no new appearances are created if the field value hasn't changed between
211
+ # invocations.
212
+ #
213
+ # By setting +force+ to +true+ the creation of the appearances can be forced.
214
+ def create_appearances(force: false)
215
+ current_value = field_value
153
216
  appearance_generator_class = document.config.constantize('acro_form.appearance_generator')
154
217
  each_widget do |widget|
218
+ next if !force && widget.cached?(:last_value) && widget.cache(:last_value) == current_value
219
+ widget.cache(:last_value, current_value, update: true)
155
220
  appearance_generator_class.new(widget).create_text_appearances
156
221
  end
157
222
  end
@@ -173,8 +238,9 @@ module HexaPDF
173
238
 
174
239
  if self[:V] && !(self[:V].kind_of?(String) || self[:V].kind_of?(HexaPDF::Stream))
175
240
  yield("Text field doesn't contain text but #{self[:V].class} object")
241
+ return
176
242
  end
177
- if (max_len = self[:MaxLen]) && field_value.length > max_len
243
+ if (max_len = self[:MaxLen]) && field_value && field_value.length > max_len
178
244
  yield("Text contents of field '#{full_field_name}' is too long")
179
245
  end
180
246
  end