hexapdf 0.12.3 → 0.14.3

Sign up to get free protection for your applications and to get access to all the features.
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