hexapdf 0.40.0 → 0.42.0

Sign up to get free protection for your applications and to get access to all the features.
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)