hexapdf 0.40.0 → 0.42.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 +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)
|