hexapdf 0.40.0 → 0.42.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 +71 -0
- data/examples/019-acro_form.rb +12 -23
- data/examples/027-composer_optional_content.rb +1 -1
- data/examples/030-pdfa.rb +6 -6
- data/examples/031-acro_form_java_script.rb +113 -0
- data/lib/hexapdf/cli/command.rb +25 -11
- data/lib/hexapdf/cli/files.rb +31 -7
- data/lib/hexapdf/cli/form.rb +46 -38
- data/lib/hexapdf/cli/info.rb +4 -0
- data/lib/hexapdf/cli/inspect.rb +1 -1
- data/lib/hexapdf/cli/usage.rb +215 -0
- data/lib/hexapdf/cli.rb +2 -0
- data/lib/hexapdf/configuration.rb +11 -1
- data/lib/hexapdf/content/canvas.rb +2 -0
- data/lib/hexapdf/document/layout.rb +8 -1
- data/lib/hexapdf/encryption/aes.rb +13 -6
- data/lib/hexapdf/encryption/security_handler.rb +6 -4
- data/lib/hexapdf/font/cmap/parser.rb +1 -5
- data/lib/hexapdf/font/cmap.rb +22 -3
- data/lib/hexapdf/font_loader/from_configuration.rb +1 -1
- data/lib/hexapdf/font_loader/variant_from_name.rb +72 -0
- data/lib/hexapdf/font_loader.rb +1 -0
- data/lib/hexapdf/layout/style.rb +5 -4
- data/lib/hexapdf/type/acro_form/appearance_generator.rb +11 -76
- data/lib/hexapdf/type/acro_form/button_field.rb +7 -5
- data/lib/hexapdf/type/acro_form/field.rb +14 -0
- data/lib/hexapdf/type/acro_form/form.rb +70 -8
- data/lib/hexapdf/type/acro_form/java_script_actions.rb +649 -0
- data/lib/hexapdf/type/acro_form/text_field.rb +90 -0
- data/lib/hexapdf/type/acro_form.rb +1 -0
- data/lib/hexapdf/type/annotations/widget.rb +1 -1
- data/lib/hexapdf/type/resources.rb +2 -1
- data/lib/hexapdf/utils.rb +19 -0
- data/lib/hexapdf/version.rb +1 -1
- data/test/hexapdf/encryption/test_aes.rb +18 -8
- data/test/hexapdf/encryption/test_security_handler.rb +17 -0
- data/test/hexapdf/encryption/test_standard_security_handler.rb +1 -1
- data/test/hexapdf/font/cmap/test_parser.rb +5 -3
- data/test/hexapdf/font/test_cmap.rb +8 -0
- data/test/hexapdf/font_loader/test_from_configuration.rb +4 -0
- data/test/hexapdf/font_loader/test_variant_from_name.rb +34 -0
- data/test/hexapdf/test_utils.rb +16 -0
- data/test/hexapdf/type/acro_form/test_appearance_generator.rb +31 -47
- data/test/hexapdf/type/acro_form/test_button_field.rb +5 -0
- data/test/hexapdf/type/acro_form/test_field.rb +11 -0
- data/test/hexapdf/type/acro_form/test_form.rb +80 -0
- data/test/hexapdf/type/acro_form/test_java_script_actions.rb +327 -0
- data/test/hexapdf/type/acro_form/test_text_field.rb +62 -0
- data/test/hexapdf/type/test_resources.rb +5 -0
- metadata +8 -2
@@ -39,6 +39,7 @@ require 'hexapdf/error'
|
|
39
39
|
require 'hexapdf/layout/style'
|
40
40
|
require 'hexapdf/layout/text_fragment'
|
41
41
|
require 'hexapdf/layout/text_layouter'
|
42
|
+
require 'hexapdf/type/acro_form/java_script_actions'
|
42
43
|
|
43
44
|
module HexaPDF
|
44
45
|
module Type
|
@@ -129,14 +130,20 @@ module HexaPDF
|
|
129
130
|
# widget.background_color(1)
|
130
131
|
# widget.marker_style(style: :circle, size: 0, color: 0)
|
131
132
|
def create_check_box_appearances
|
132
|
-
|
133
|
-
|
133
|
+
normal_appearance = @widget.appearance_dict&.normal_appearance
|
134
|
+
if !normal_appearance.kind_of?(HexaPDF::Dictionary) || normal_appearance.kind_of?(HexaPDF::Stream)
|
135
|
+
(@widget[:AP] ||= {})[:N] = {Off: nil}
|
136
|
+
normal_appearance = @widget[:AP][:N]
|
137
|
+
normal_appearance[@field[:V] == :Off ? :Yes : @field[:V]] = nil
|
138
|
+
end
|
139
|
+
on_name = (normal_appearance.value.keys - [:Off]).first
|
134
140
|
unless on_name
|
135
141
|
raise HexaPDF::Error, "Widget of button field doesn't define name for on state"
|
136
142
|
end
|
137
143
|
|
138
144
|
@widget[:AS] = (@field[:V] == on_name ? on_name : :Off)
|
139
145
|
@widget.flag(:print)
|
146
|
+
@widget.unflag(:hidden)
|
140
147
|
|
141
148
|
border_style = @widget.border_style
|
142
149
|
marker_style = @widget.marker_style
|
@@ -206,6 +213,7 @@ module HexaPDF
|
|
206
213
|
|
207
214
|
@widget[:AS] = :N
|
208
215
|
@widget.flag(:print)
|
216
|
+
@widget.unflag(:hidden)
|
209
217
|
rect = @widget[:Rect]
|
210
218
|
rect.width = @document.config['acro_form.text_field.default_width'] if rect.width == 0
|
211
219
|
if rect.height == 0
|
@@ -358,7 +366,7 @@ module HexaPDF
|
|
358
366
|
|
359
367
|
# Draws a single line of text inside the widget's rectangle.
|
360
368
|
def draw_single_line_text(canvas, width, height, style, padding)
|
361
|
-
value, text_color =
|
369
|
+
value, text_color = JavaScriptActions.apply_format(@field.field_value, @field[:AA]&.[](:F))
|
362
370
|
style.fill_color = text_color if text_color
|
363
371
|
calculate_and_apply_font_size(value, style, width, height, padding)
|
364
372
|
line = HexaPDF::Layout::Line.new(@document.layout.text_fragments(value, style: style))
|
@@ -500,79 +508,6 @@ module HexaPDF
|
|
500
508
|
style.clear_cache
|
501
509
|
end
|
502
510
|
|
503
|
-
# Handles Javascript formatting routines for single-line text fields.
|
504
|
-
#
|
505
|
-
# Returns [value, nil_or_text_color] where value is the new, potentially adjusted field
|
506
|
-
# value and the second argument is either +nil+ or the color that should be used for the
|
507
|
-
# text value.
|
508
|
-
def apply_javascript_formatting(value)
|
509
|
-
format_action = @widget[:AA]&.[](:F)
|
510
|
-
return [value, nil] unless format_action && format_action[:S] == :JavaScript
|
511
|
-
if (match = AF_NUMBER_FORMAT_RE.match(format_action[:JS]))
|
512
|
-
apply_af_number_format(value, match)
|
513
|
-
else
|
514
|
-
[value, nil]
|
515
|
-
end
|
516
|
-
end
|
517
|
-
|
518
|
-
# Regular expression for matching the AFNumber_Format Javascript method.
|
519
|
-
AF_NUMBER_FORMAT_RE = /
|
520
|
-
\AAFNumber_Format\(
|
521
|
-
\s*(?<ndec>\d+)\s*,
|
522
|
-
\s*(?<sep_style>[0-3])\s*,
|
523
|
-
\s*(?<neg_style>[0-3])\s*,
|
524
|
-
\s*0\s*,
|
525
|
-
\s*(?<currency_string>".*?")\s*,
|
526
|
-
\s*(?<prepend>false|true)\s*
|
527
|
-
\);\z
|
528
|
-
/x
|
529
|
-
|
530
|
-
# Implements the Javascript AFNumber_Format method.
|
531
|
-
#
|
532
|
-
# See:
|
533
|
-
# - https://experienceleague.adobe.com/docs/experience-manager-learn/assets/FormsAPIReference.pdf
|
534
|
-
# - https://opensource.adobe.com/dc-acrobat-sdk-docs/library/jsapiref/JS_API_AcroJS.html#printf
|
535
|
-
def apply_af_number_format(value, match)
|
536
|
-
value = value.to_f
|
537
|
-
format = "%.#{match[:ndec]}f"
|
538
|
-
text_color = 'black'
|
539
|
-
|
540
|
-
currency_string = JSON.parse(match[:currency_string])
|
541
|
-
format = (match[:prepend] == 'true' ? currency_string + format : format + currency_string)
|
542
|
-
|
543
|
-
if value < 0
|
544
|
-
value = value.abs
|
545
|
-
case match[:neg_style]
|
546
|
-
when '0' # MinusBlack
|
547
|
-
format = "-#{format}"
|
548
|
-
when '1' # Red
|
549
|
-
text_color = 'red'
|
550
|
-
when '2' # ParensBlack
|
551
|
-
format = "(#{format})"
|
552
|
-
when '3' # ParensRed
|
553
|
-
format = "(#{format})"
|
554
|
-
text_color = 'red'
|
555
|
-
end
|
556
|
-
end
|
557
|
-
|
558
|
-
result = sprintf(format, value)
|
559
|
-
|
560
|
-
# sep_style: 0=12,345.67, 1=12345.67, 2=12.345,67, 3=12345,67
|
561
|
-
before_decimal_point, after_decimal_point = result.split('.')
|
562
|
-
if match[:sep_style] == '0' || match[:sep_style] == '2'
|
563
|
-
separator = (match[:sep_style] == '0' ? ',' : '.')
|
564
|
-
before_decimal_point.gsub!(/\B(?=(\d\d\d)+(?:[^\d]|\z))/, separator)
|
565
|
-
end
|
566
|
-
result = if after_decimal_point
|
567
|
-
decimal_point = (match[:sep_style] =~ /[01]/ ? '.' : ',')
|
568
|
-
"#{before_decimal_point}#{decimal_point}#{after_decimal_point}"
|
569
|
-
else
|
570
|
-
before_decimal_point
|
571
|
-
end
|
572
|
-
|
573
|
-
[result, text_color]
|
574
|
-
end
|
575
|
-
|
576
511
|
end
|
577
512
|
|
578
513
|
end
|
@@ -165,14 +165,16 @@ module HexaPDF
|
|
165
165
|
# nothing is stored for them (e.g a no-op).
|
166
166
|
#
|
167
167
|
# Check boxes:: Provide +nil+ or +false+ as value to toggle all check box widgets off. If
|
168
|
-
#
|
169
|
-
#
|
170
|
-
#
|
171
|
-
# toggled on.
|
168
|
+
# +true+ is provided, all check box widgets with the same name as the first
|
169
|
+
# one are toggled on. Otherwise provide the value (a Symbol or an object
|
170
|
+
# responding to +#to_sym+) of the check box widget that should be toggled on.
|
172
171
|
#
|
173
172
|
# Radio buttons:: To turn all radio buttons off, provide +nil+ as value. Otherwise provide
|
174
173
|
# the value (a Symbol or an object responding to +#to_sym+) of a radio
|
175
174
|
# button that should be turned on.
|
175
|
+
#
|
176
|
+
# Note that in most cases the field needs to already have widgets because the value is
|
177
|
+
# checked against the possibly allowed values which depend on the existing widgets.
|
176
178
|
def field_value=(value)
|
177
179
|
normalized_field_value_set(:V, value)
|
178
180
|
end
|
@@ -303,7 +305,7 @@ module HexaPDF
|
|
303
305
|
elsif check_box?
|
304
306
|
if value == false
|
305
307
|
:Off
|
306
|
-
elsif value == true && av.size
|
308
|
+
elsif value == true && av.size >= 1
|
307
309
|
av[0]
|
308
310
|
elsif av.include?(value.to_sym)
|
309
311
|
value.to_sym
|
@@ -155,6 +155,12 @@ module HexaPDF
|
|
155
155
|
field.value[name].nil? ? nil : field[name]
|
156
156
|
end
|
157
157
|
|
158
|
+
# Wraps the given +field+ object inside the correct field class and returns the wrapped
|
159
|
+
# object.
|
160
|
+
def self.wrap(document, field)
|
161
|
+
document.wrap(field, type: :XXAcroFormField, subtype: inherited_value(field, :FT))
|
162
|
+
end
|
163
|
+
|
158
164
|
# Form fields must always be indirect objects.
|
159
165
|
def must_be_indirect?
|
160
166
|
true
|
@@ -236,6 +242,14 @@ module HexaPDF
|
|
236
242
|
kids.nil? || kids.empty? || kids.none? {|kid| kid.key?(:T) }
|
237
243
|
end
|
238
244
|
|
245
|
+
# Returns self.
|
246
|
+
#
|
247
|
+
# This method is only here to make it easier to get the form field when the object may
|
248
|
+
# either be a form field or a field widget.
|
249
|
+
def form_field
|
250
|
+
self
|
251
|
+
end
|
252
|
+
|
239
253
|
# Returns +true+ if the field contains an embedded widget.
|
240
254
|
def embedded_widget?
|
241
255
|
key?(:Subtype)
|
@@ -99,11 +99,11 @@ module HexaPDF
|
|
99
99
|
document.pages.each do |page|
|
100
100
|
page.each_annotation do |annot|
|
101
101
|
if !annot.key?(:Parent) && annot.key?(:FT)
|
102
|
-
result <<
|
102
|
+
result << Field.wrap(document, annot)
|
103
103
|
elsif annot.key?(:Parent)
|
104
104
|
field = annot[:Parent]
|
105
105
|
field = field[:Parent] while field[:Parent]
|
106
|
-
result <<
|
106
|
+
result << Field.wrap(document, field)
|
107
107
|
end
|
108
108
|
end
|
109
109
|
end
|
@@ -129,8 +129,7 @@ module HexaPDF
|
|
129
129
|
array.each_with_index do |field, index|
|
130
130
|
next if field.nil?
|
131
131
|
unless field.respond_to?(:type) && field.type == :XXAcroFormField
|
132
|
-
array[index] = field =
|
133
|
-
subtype: Field.inherited_value(field, :FT))
|
132
|
+
array[index] = field = Field.wrap(document, field)
|
134
133
|
end
|
135
134
|
if field.terminal_field?
|
136
135
|
yield(field)
|
@@ -153,8 +152,7 @@ module HexaPDF
|
|
153
152
|
name.split('.').each do |part|
|
154
153
|
field = nil
|
155
154
|
fields&.each do |f|
|
156
|
-
f =
|
157
|
-
subtype: Field.inherited_value(f, :FT))
|
155
|
+
f = Field.wrap(document, f)
|
158
156
|
next unless f[:T] == part
|
159
157
|
field = f
|
160
158
|
fields = field[:Kids] unless field.terminal_field?
|
@@ -274,6 +272,9 @@ module HexaPDF
|
|
274
272
|
#
|
275
273
|
# The +name+ may contain dots to signify a field hierarchy. If so, the referenced parent
|
276
274
|
# fields must already exist. If it doesn't contain dots, a top-level field is created.
|
275
|
+
#
|
276
|
+
# Before a field value other than +false+ can be assigned to the check box, a widget needs
|
277
|
+
# to be created.
|
277
278
|
def create_check_box(name)
|
278
279
|
create_field(name, :Btn, &:initialize_as_check_box)
|
279
280
|
end
|
@@ -282,6 +283,9 @@ module HexaPDF
|
|
282
283
|
#
|
283
284
|
# The +name+ may contain dots to signify a field hierarchy. If so, the referenced parent
|
284
285
|
# fields must already exist. If it doesn't contain dots, a top-level field is created.
|
286
|
+
#
|
287
|
+
# Before a field value other than +nil+ can be assigned to the radio button, at least one
|
288
|
+
# widget needs to be created.
|
285
289
|
def create_radio_button(name)
|
286
290
|
create_field(name, :Btn, &:initialize_as_radio_button)
|
287
291
|
end
|
@@ -347,6 +351,45 @@ module HexaPDF
|
|
347
351
|
create_field(name, :Sig) {}
|
348
352
|
end
|
349
353
|
|
354
|
+
# Fills form fields with the values from the given +data+ hash.
|
355
|
+
#
|
356
|
+
# The keys of the +data+ hash need to be full field names and the values are the respective
|
357
|
+
# values, usually in string form. It is possible to specify only some of the fields of the
|
358
|
+
# form.
|
359
|
+
#
|
360
|
+
# What kind of values are supported for a field depends on the field type:
|
361
|
+
#
|
362
|
+
# * For fields containing text (single/multiline/comb text fields, file select fields, combo
|
363
|
+
# boxes and list boxes) the value needs to be a string and it is assigned as is.
|
364
|
+
#
|
365
|
+
# * For check boxes, the values "y"/"yes"/"t"/"true" are handled as assigning +true+ to the
|
366
|
+
# field, the values "n"/"no"/"f"/"false" are handled as assigning +false+ to the field,
|
367
|
+
# and every other string value is assigned as is. See ButtonField#field_value= for
|
368
|
+
# details.
|
369
|
+
#
|
370
|
+
# * For radio buttons the value needs to be a String or a Symbol representing the name of
|
371
|
+
# the radio button widget to select.
|
372
|
+
def fill(data)
|
373
|
+
data.each do |field_name, value|
|
374
|
+
field = field_by_name(field_name)
|
375
|
+
raise HexaPDF::Error, "AcroForm field named '#{field_name}' not found" unless field
|
376
|
+
|
377
|
+
case field.concrete_field_type
|
378
|
+
when :single_line_text_field, :multiline_text_field, :comb_text_field, :file_select_field,
|
379
|
+
:combo_box, :list_box, :editable_combo_box, :radio_button
|
380
|
+
field.field_value = value
|
381
|
+
when :check_box
|
382
|
+
field.field_value = case value
|
383
|
+
when /\A(?:y(es)?|t(rue)?)\z/ then true
|
384
|
+
when /\A(?:n(o)?|f(alse)?)\z/ then false
|
385
|
+
else value
|
386
|
+
end
|
387
|
+
else
|
388
|
+
raise HexaPDF::Error, "AcroForm field type #{field.concrete_field_type} not yet supported"
|
389
|
+
end
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
350
393
|
# Returns the dictionary containing the default resources for form field appearance streams.
|
351
394
|
def default_resources
|
352
395
|
self[:DR] ||= document.wrap({ProcSet: [:PDF, :Text, :ImageB, :ImageC, :ImageI]},
|
@@ -420,6 +463,26 @@ module HexaPDF
|
|
420
463
|
not_flattened
|
421
464
|
end
|
422
465
|
|
466
|
+
# Recalculates all form fields that have a calculate action applied (which are all fields
|
467
|
+
# listed in the /CO entry).
|
468
|
+
#
|
469
|
+
# If HexaPDF doesn't support a calculation method or an error occurs during calculation, the
|
470
|
+
# field value is not updated.
|
471
|
+
#
|
472
|
+
# Note that calculations are *not* done automatically when a form field's value changes
|
473
|
+
# since it would lead to possibly many calls to this actions. So first fill in all field
|
474
|
+
# values and then call this method.
|
475
|
+
#
|
476
|
+
# See: JavaScriptActions
|
477
|
+
def recalculate_fields
|
478
|
+
self[:CO]&.each do |field|
|
479
|
+
field = Field.wrap(document, field)
|
480
|
+
next unless field && (calculation_action = field[:AA]&.[](:C))
|
481
|
+
result = JavaScriptActions.calculate(self, calculation_action)
|
482
|
+
field.form_field.field_value = result if result
|
483
|
+
end
|
484
|
+
end
|
485
|
+
|
423
486
|
private
|
424
487
|
|
425
488
|
# Creates a new field with the full name +name+ and the field type +type+.
|
@@ -468,8 +531,7 @@ module HexaPDF
|
|
468
531
|
end
|
469
532
|
next false unless field.key?(:T) # Skip widgets
|
470
533
|
|
471
|
-
field =
|
472
|
-
subtype: Field.inherited_value(field, :FT))
|
534
|
+
field = Field.wrap(document, field)
|
473
535
|
reject = false
|
474
536
|
if field[:Parent] != parent
|
475
537
|
yield("Parent entry of field (#{field.oid},#{field.gen}) invalid", true)
|