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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +71 -0
  3. data/examples/019-acro_form.rb +12 -23
  4. data/examples/027-composer_optional_content.rb +1 -1
  5. data/examples/030-pdfa.rb +6 -6
  6. data/examples/031-acro_form_java_script.rb +113 -0
  7. data/lib/hexapdf/cli/command.rb +25 -11
  8. data/lib/hexapdf/cli/files.rb +31 -7
  9. data/lib/hexapdf/cli/form.rb +46 -38
  10. data/lib/hexapdf/cli/info.rb +4 -0
  11. data/lib/hexapdf/cli/inspect.rb +1 -1
  12. data/lib/hexapdf/cli/usage.rb +215 -0
  13. data/lib/hexapdf/cli.rb +2 -0
  14. data/lib/hexapdf/configuration.rb +11 -1
  15. data/lib/hexapdf/content/canvas.rb +2 -0
  16. data/lib/hexapdf/document/layout.rb +8 -1
  17. data/lib/hexapdf/encryption/aes.rb +13 -6
  18. data/lib/hexapdf/encryption/security_handler.rb +6 -4
  19. data/lib/hexapdf/font/cmap/parser.rb +1 -5
  20. data/lib/hexapdf/font/cmap.rb +22 -3
  21. data/lib/hexapdf/font_loader/from_configuration.rb +1 -1
  22. data/lib/hexapdf/font_loader/variant_from_name.rb +72 -0
  23. data/lib/hexapdf/font_loader.rb +1 -0
  24. data/lib/hexapdf/layout/style.rb +5 -4
  25. data/lib/hexapdf/type/acro_form/appearance_generator.rb +11 -76
  26. data/lib/hexapdf/type/acro_form/button_field.rb +7 -5
  27. data/lib/hexapdf/type/acro_form/field.rb +14 -0
  28. data/lib/hexapdf/type/acro_form/form.rb +70 -8
  29. data/lib/hexapdf/type/acro_form/java_script_actions.rb +649 -0
  30. data/lib/hexapdf/type/acro_form/text_field.rb +90 -0
  31. data/lib/hexapdf/type/acro_form.rb +1 -0
  32. data/lib/hexapdf/type/annotations/widget.rb +1 -1
  33. data/lib/hexapdf/type/resources.rb +2 -1
  34. data/lib/hexapdf/utils.rb +19 -0
  35. data/lib/hexapdf/version.rb +1 -1
  36. data/test/hexapdf/encryption/test_aes.rb +18 -8
  37. data/test/hexapdf/encryption/test_security_handler.rb +17 -0
  38. data/test/hexapdf/encryption/test_standard_security_handler.rb +1 -1
  39. data/test/hexapdf/font/cmap/test_parser.rb +5 -3
  40. data/test/hexapdf/font/test_cmap.rb +8 -0
  41. data/test/hexapdf/font_loader/test_from_configuration.rb +4 -0
  42. data/test/hexapdf/font_loader/test_variant_from_name.rb +34 -0
  43. data/test/hexapdf/test_utils.rb +16 -0
  44. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +31 -47
  45. data/test/hexapdf/type/acro_form/test_button_field.rb +5 -0
  46. data/test/hexapdf/type/acro_form/test_field.rb +11 -0
  47. data/test/hexapdf/type/acro_form/test_form.rb +80 -0
  48. data/test/hexapdf/type/acro_form/test_java_script_actions.rb +327 -0
  49. data/test/hexapdf/type/acro_form/test_text_field.rb +62 -0
  50. data/test/hexapdf/type/test_resources.rb +5 -0
  51. 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
- appearance_keys = @widget.appearance_dict&.normal_appearance&.value&.keys || []
133
- on_name = (appearance_keys - [:Off]).first
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 = apply_javascript_formatting(@field.field_value)
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
- # there is only one possible value, +true+ may be used for checking the box,
169
- # i.e. toggling it to the on state. Otherwise provide the value (a Symbol or
170
- # an object responding to +#to_sym+) of the check box widget that should be
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 == 1
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 << document.wrap(annot, type: :XXAcroFormField, subtype: annot[:FT])
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 << document.wrap(field, type: :XXAcroFormField)
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 = document.wrap(field, type: :XXAcroFormField,
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 = document.wrap(f, type: :XXAcroFormField,
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 = document.wrap(field, type: :XXAcroFormField,
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)