hexapdf 0.13.0 → 0.14.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +51 -0
- data/examples/019-acro_form.rb +41 -4
- data/lib/hexapdf/cli/split.rb +74 -14
- data/lib/hexapdf/document.rb +10 -4
- data/lib/hexapdf/layout/text_layouter.rb +2 -2
- data/lib/hexapdf/object.rb +22 -0
- data/lib/hexapdf/parser.rb +23 -1
- data/lib/hexapdf/pdf_array.rb +2 -2
- data/lib/hexapdf/type/acro_form/appearance_generator.rb +127 -27
- data/lib/hexapdf/type/acro_form/button_field.rb +5 -2
- data/lib/hexapdf/type/acro_form/choice_field.rb +64 -10
- data/lib/hexapdf/type/acro_form/form.rb +133 -10
- data/lib/hexapdf/type/acro_form/text_field.rb +68 -3
- data/lib/hexapdf/type/cid_font.rb +1 -1
- data/lib/hexapdf/type/font.rb +1 -1
- data/lib/hexapdf/type/font_simple.rb +1 -1
- data/lib/hexapdf/type/font_type0.rb +3 -3
- data/lib/hexapdf/type/form.rb +4 -1
- data/lib/hexapdf/type/page.rb +5 -5
- data/lib/hexapdf/utils/object_hash.rb +0 -1
- data/lib/hexapdf/version.rb +1 -1
- data/test/hexapdf/layout/test_text_layouter.rb +9 -1
- data/test/hexapdf/test_document.rb +14 -6
- data/test/hexapdf/test_object.rb +27 -0
- data/test/hexapdf/test_parser.rb +46 -0
- data/test/hexapdf/test_pdf_array.rb +1 -1
- data/test/hexapdf/test_writer.rb +2 -2
- data/test/hexapdf/type/acro_form/test_appearance_generator.rb +286 -34
- 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_form.rb +83 -11
- data/test/hexapdf/type/acro_form/test_text_field.rb +75 -1
- data/test/hexapdf/type/test_form.rb +7 -0
- data/test/hexapdf/utils/test_object_hash.rb +5 -0
- data/test/test_helper.rb +2 -0
- 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
|
-
|
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.
|
@@ -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])
|
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
|
@@ -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,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
|
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.
|
@@ -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
|
-
|
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
|
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
|
data/lib/hexapdf/type/font.rb
CHANGED
@@ -102,7 +102,7 @@ module HexaPDF
|
|
102
102
|
|
103
103
|
# Parses and caches the ToUnicode CMap.
|
104
104
|
def to_unicode_cmap
|
105
|
-
|
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
|
-
|
60
|
+
cache(:encoding) do
|
61
61
|
case (val = self[:Encoding])
|
62
62
|
when Symbol
|
63
63
|
encoding = HexaPDF::Font::Encoding.for_name(val)
|