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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +132 -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/dictionary.rb +12 -6
- data/lib/hexapdf/dictionary_fields.rb +2 -10
- 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 +16 -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 +107 -7
- data/lib/hexapdf/pdf_array.rb +15 -5
- data/lib/hexapdf/revisions.rb +29 -21
- data/lib/hexapdf/serializer.rb +37 -10
- data/lib/hexapdf/task/optimize.rb +6 -4
- data/lib/hexapdf/tokenizer.rb +22 -0
- data/lib/hexapdf/type/acro_form/appearance_generator.rb +130 -27
- data/lib/hexapdf/type/acro_form/button_field.rb +5 -2
- 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 +6 -2
- data/lib/hexapdf/type/image.rb +2 -2
- data/lib/hexapdf/type/page.rb +21 -12
- data/lib/hexapdf/type/page_tree_node.rb +29 -5
- data/lib/hexapdf/type/resources.rb +5 -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 +2 -2
- 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 +10 -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_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 +9 -2
- data/test/hexapdf/test_document.rb +18 -10
- data/test/hexapdf/test_object.rb +71 -26
- data/test/hexapdf/test_parser.rb +205 -51
- data/test/hexapdf/test_pdf_array.rb +8 -1
- data/test/hexapdf/test_revisions.rb +35 -0
- data/test/hexapdf/test_serializer.rb +7 -0
- data/test/hexapdf/test_tokenizer.rb +28 -0
- data/test/hexapdf/test_writer.rb +2 -2
- data/test/hexapdf/type/acro_form/test_appearance_generator.rb +288 -35
- data/test/hexapdf/type/acro_form/test_button_field.rb +15 -0
- 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 +8 -1
- data/test/hexapdf/type/test_page.rb +8 -1
- data/test/hexapdf/type/test_page_tree_node.rb +42 -0
- data/test/hexapdf/type/test_resources.rb +6 -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 -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
|
-
|
|
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
|
-
|
|
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
|