hexapdf 0.13.0 → 0.14.0
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 +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)
|