hexapdf 0.12.1 → 0.14.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +130 -0
- data/examples/019-acro_form.rb +41 -4
- data/lib/hexapdf/cli/command.rb +4 -2
- data/lib/hexapdf/cli/image2pdf.rb +2 -1
- data/lib/hexapdf/cli/info.rb +51 -2
- data/lib/hexapdf/cli/inspect.rb +30 -8
- data/lib/hexapdf/cli/merge.rb +1 -1
- data/lib/hexapdf/cli/split.rb +74 -14
- data/lib/hexapdf/configuration.rb +15 -0
- data/lib/hexapdf/content/graphic_object/arc.rb +3 -3
- data/lib/hexapdf/content/parser.rb +1 -1
- data/lib/hexapdf/dictionary.rb +9 -6
- data/lib/hexapdf/dictionary_fields.rb +1 -9
- data/lib/hexapdf/document.rb +41 -16
- data/lib/hexapdf/document/files.rb +0 -1
- data/lib/hexapdf/encryption/fast_arc4.rb +1 -1
- data/lib/hexapdf/encryption/security_handler.rb +1 -0
- data/lib/hexapdf/encryption/standard_security_handler.rb +1 -0
- data/lib/hexapdf/font/cmap.rb +1 -4
- data/lib/hexapdf/font/true_type/subsetter.rb +12 -3
- data/lib/hexapdf/font/true_type/table/head.rb +1 -0
- data/lib/hexapdf/font/true_type/table/os2.rb +2 -0
- data/lib/hexapdf/font/true_type/table/post.rb +15 -10
- data/lib/hexapdf/font_loader/from_configuration.rb +2 -2
- data/lib/hexapdf/font_loader/from_file.rb +18 -8
- data/lib/hexapdf/image_loader/png.rb +3 -2
- data/lib/hexapdf/importer.rb +3 -2
- data/lib/hexapdf/layout/line.rb +1 -1
- data/lib/hexapdf/layout/style.rb +23 -23
- data/lib/hexapdf/layout/text_layouter.rb +2 -2
- data/lib/hexapdf/layout/text_shaper.rb +3 -2
- data/lib/hexapdf/object.rb +52 -25
- data/lib/hexapdf/parser.rb +96 -4
- data/lib/hexapdf/pdf_array.rb +12 -5
- data/lib/hexapdf/revisions.rb +29 -21
- data/lib/hexapdf/serializer.rb +34 -8
- data/lib/hexapdf/task/optimize.rb +6 -4
- data/lib/hexapdf/tokenizer.rb +4 -3
- data/lib/hexapdf/type/acro_form/appearance_generator.rb +132 -28
- data/lib/hexapdf/type/acro_form/button_field.rb +21 -13
- data/lib/hexapdf/type/acro_form/choice_field.rb +68 -14
- data/lib/hexapdf/type/acro_form/field.rb +35 -5
- data/lib/hexapdf/type/acro_form/form.rb +139 -14
- data/lib/hexapdf/type/acro_form/text_field.rb +70 -4
- data/lib/hexapdf/type/actions/uri.rb +3 -2
- data/lib/hexapdf/type/annotations/widget.rb +3 -4
- data/lib/hexapdf/type/catalog.rb +2 -2
- data/lib/hexapdf/type/cid_font.rb +1 -1
- data/lib/hexapdf/type/file_specification.rb +1 -1
- data/lib/hexapdf/type/font.rb +1 -1
- data/lib/hexapdf/type/font_simple.rb +4 -2
- data/lib/hexapdf/type/font_true_type.rb +6 -2
- data/lib/hexapdf/type/font_type0.rb +4 -4
- data/lib/hexapdf/type/form.rb +15 -2
- data/lib/hexapdf/type/image.rb +2 -2
- data/lib/hexapdf/type/page.rb +37 -13
- data/lib/hexapdf/type/page_tree_node.rb +29 -5
- data/lib/hexapdf/type/resources.rb +1 -0
- data/lib/hexapdf/type/trailer.rb +2 -3
- data/lib/hexapdf/utils/object_hash.rb +0 -1
- data/lib/hexapdf/utils/sorted_tree_node.rb +18 -15
- data/lib/hexapdf/version.rb +1 -1
- data/test/hexapdf/common_tokenizer_tests.rb +6 -1
- data/test/hexapdf/content/graphic_object/test_arc.rb +4 -4
- data/test/hexapdf/content/test_canvas.rb +3 -3
- data/test/hexapdf/content/test_color_space.rb +1 -1
- data/test/hexapdf/encryption/test_aes.rb +4 -4
- data/test/hexapdf/encryption/test_standard_security_handler.rb +11 -11
- data/test/hexapdf/filter/test_ascii85_decode.rb +1 -1
- data/test/hexapdf/filter/test_ascii_hex_decode.rb +1 -1
- data/test/hexapdf/font/true_type/table/test_post.rb +1 -1
- data/test/hexapdf/font/true_type/test_subsetter.rb +5 -0
- data/test/hexapdf/font_loader/test_from_configuration.rb +7 -3
- data/test/hexapdf/font_loader/test_from_file.rb +7 -0
- data/test/hexapdf/layout/test_style.rb +1 -1
- data/test/hexapdf/layout/test_text_layouter.rb +12 -5
- data/test/hexapdf/test_configuration.rb +2 -2
- data/test/hexapdf/test_dictionary.rb +8 -1
- data/test/hexapdf/test_dictionary_fields.rb +2 -2
- data/test/hexapdf/test_document.rb +18 -10
- data/test/hexapdf/test_object.rb +71 -26
- data/test/hexapdf/test_parser.rb +171 -53
- data/test/hexapdf/test_pdf_array.rb +8 -1
- data/test/hexapdf/test_revisions.rb +35 -0
- data/test/hexapdf/test_writer.rb +2 -2
- data/test/hexapdf/type/acro_form/test_appearance_generator.rb +296 -38
- data/test/hexapdf/type/acro_form/test_button_field.rb +22 -2
- data/test/hexapdf/type/acro_form/test_choice_field.rb +92 -9
- data/test/hexapdf/type/acro_form/test_field.rb +39 -0
- data/test/hexapdf/type/acro_form/test_form.rb +87 -15
- data/test/hexapdf/type/acro_form/test_text_field.rb +77 -1
- data/test/hexapdf/type/test_font_simple.rb +2 -1
- data/test/hexapdf/type/test_font_true_type.rb +6 -0
- data/test/hexapdf/type/test_form.rb +26 -1
- data/test/hexapdf/type/test_page.rb +45 -7
- data/test/hexapdf/type/test_page_tree_node.rb +42 -0
- data/test/hexapdf/utils/test_bit_field.rb +2 -0
- data/test/hexapdf/utils/test_object_hash.rb +5 -0
- data/test/hexapdf/utils/test_sorted_tree_node.rb +10 -9
- data/test/test_helper.rb +2 -0
- metadata +6 -11
@@ -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
|
-
|
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
|
-
|
130
|
-
|
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
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
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])
|
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] =
|
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
|
182
|
-
#
|
183
|
-
|
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
|
-
|
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
|
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 ||
|
283
|
+
if !allow_embedded || embedded_widget? || (key?(:Kids) && !self[:Kids].empty?)
|
279
284
|
kids = self[:Kids] ||= []
|
280
|
-
kids << extract_widget if
|
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
|
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
|
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
|
-
|
157
|
-
|
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
|
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
|
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
|
-
|
181
|
-
|
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
|
-
|
189
|
-
|
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({
|
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
|
-
|
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)
|
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
|
151
|
-
#
|
152
|
-
|
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
|