hexapdf 0.13.0 → 0.14.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +51 -0
  3. data/examples/019-acro_form.rb +41 -4
  4. data/lib/hexapdf/cli/split.rb +74 -14
  5. data/lib/hexapdf/document.rb +10 -4
  6. data/lib/hexapdf/layout/text_layouter.rb +2 -2
  7. data/lib/hexapdf/object.rb +22 -0
  8. data/lib/hexapdf/parser.rb +23 -1
  9. data/lib/hexapdf/pdf_array.rb +2 -2
  10. data/lib/hexapdf/type/acro_form/appearance_generator.rb +127 -27
  11. data/lib/hexapdf/type/acro_form/button_field.rb +5 -2
  12. data/lib/hexapdf/type/acro_form/choice_field.rb +64 -10
  13. data/lib/hexapdf/type/acro_form/form.rb +133 -10
  14. data/lib/hexapdf/type/acro_form/text_field.rb +68 -3
  15. data/lib/hexapdf/type/cid_font.rb +1 -1
  16. data/lib/hexapdf/type/font.rb +1 -1
  17. data/lib/hexapdf/type/font_simple.rb +1 -1
  18. data/lib/hexapdf/type/font_type0.rb +3 -3
  19. data/lib/hexapdf/type/form.rb +4 -1
  20. data/lib/hexapdf/type/page.rb +5 -5
  21. data/lib/hexapdf/utils/object_hash.rb +0 -1
  22. data/lib/hexapdf/version.rb +1 -1
  23. data/test/hexapdf/layout/test_text_layouter.rb +9 -1
  24. data/test/hexapdf/test_document.rb +14 -6
  25. data/test/hexapdf/test_object.rb +27 -0
  26. data/test/hexapdf/test_parser.rb +46 -0
  27. data/test/hexapdf/test_pdf_array.rb +1 -1
  28. data/test/hexapdf/test_writer.rb +2 -2
  29. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +286 -34
  30. data/test/hexapdf/type/acro_form/test_button_field.rb +15 -0
  31. data/test/hexapdf/type/acro_form/test_choice_field.rb +92 -9
  32. data/test/hexapdf/type/acro_form/test_form.rb +83 -11
  33. data/test/hexapdf/type/acro_form/test_text_field.rb +75 -1
  34. data/test/hexapdf/type/test_form.rb +7 -0
  35. data/test/hexapdf/utils/test_object_hash.rb +5 -0
  36. data/test/test_helper.rb +2 -0
  37. metadata +4 -3
@@ -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.
@@ -155,13 +162,47 @@ module HexaPDF
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
@@ -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,23 +245,58 @@ 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.
@@ -213,9 +323,11 @@ module HexaPDF
213
323
  end
214
324
 
215
325
  # Creates the appearances for all widgets of all terminal fields if they don't exist.
216
- 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)
217
329
  each_field do |field|
218
- field.create_appearances if field.respond_to?(:create_appearances)
330
+ field.create_appearances(force: force) if field.respond_to?(:create_appearances)
219
331
  end
220
332
  end
221
333
 
@@ -246,9 +358,20 @@ module HexaPDF
246
358
  else
247
359
  (self[:Fields] ||= []) << field
248
360
  end
361
+
362
+ yield(field)
363
+
249
364
  field
250
365
  end
251
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
+
252
375
  def perform_validation # :nodoc:
253
376
  super
254
377
 
@@ -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
@@ -95,7 +95,7 @@ module HexaPDF
95
95
  #
96
96
  # See: PDF1.7 s9.7.4.3
97
97
  def widths
98
- document.cache(@data, :widths) do
98
+ cache(:widths) do
99
99
  result = {}
100
100
  index = 0
101
101
  array = self[:W] || []
@@ -102,7 +102,7 @@ module HexaPDF
102
102
 
103
103
  # Parses and caches the ToUnicode CMap.
104
104
  def to_unicode_cmap
105
- document.cache(@data, :to_unicode_cmap) do
105
+ cache(:to_unicode_cmap) do
106
106
  if key?(:ToUnicode)
107
107
  HexaPDF::Font::CMap.parse(self[:ToUnicode].stream)
108
108
  else
@@ -57,7 +57,7 @@ module HexaPDF
57
57
  #
58
58
  # Note that the encoding is cached internally when accessed the first time.
59
59
  def encoding
60
- document.cache(@data, :encoding) do
60
+ cache(:encoding) do
61
61
  case (val = self[:Encoding])
62
62
  when Symbol
63
63
  encoding = HexaPDF::Font::Encoding.for_name(val)