hexapdf 0.40.0 → 0.41.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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +45 -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 +101 -0
  7. data/lib/hexapdf/cli/command.rb +11 -0
  8. data/lib/hexapdf/cli/form.rb +38 -9
  9. data/lib/hexapdf/cli/info.rb +4 -0
  10. data/lib/hexapdf/configuration.rb +10 -0
  11. data/lib/hexapdf/content/canvas.rb +2 -0
  12. data/lib/hexapdf/document/layout.rb +8 -1
  13. data/lib/hexapdf/encryption/aes.rb +13 -6
  14. data/lib/hexapdf/encryption/security_handler.rb +6 -4
  15. data/lib/hexapdf/font/cmap/parser.rb +1 -5
  16. data/lib/hexapdf/font/cmap.rb +22 -3
  17. data/lib/hexapdf/font_loader/from_configuration.rb +1 -1
  18. data/lib/hexapdf/font_loader/variant_from_name.rb +72 -0
  19. data/lib/hexapdf/font_loader.rb +1 -0
  20. data/lib/hexapdf/layout/style.rb +5 -4
  21. data/lib/hexapdf/type/acro_form/appearance_generator.rb +11 -76
  22. data/lib/hexapdf/type/acro_form/field.rb +14 -0
  23. data/lib/hexapdf/type/acro_form/form.rb +25 -8
  24. data/lib/hexapdf/type/acro_form/java_script_actions.rb +498 -0
  25. data/lib/hexapdf/type/acro_form/text_field.rb +78 -0
  26. data/lib/hexapdf/type/acro_form.rb +1 -0
  27. data/lib/hexapdf/type/annotations/widget.rb +1 -1
  28. data/lib/hexapdf/version.rb +1 -1
  29. data/test/hexapdf/encryption/test_aes.rb +18 -8
  30. data/test/hexapdf/encryption/test_security_handler.rb +17 -0
  31. data/test/hexapdf/encryption/test_standard_security_handler.rb +1 -1
  32. data/test/hexapdf/font/cmap/test_parser.rb +5 -3
  33. data/test/hexapdf/font/test_cmap.rb +8 -0
  34. data/test/hexapdf/font_loader/test_from_configuration.rb +4 -0
  35. data/test/hexapdf/font_loader/test_variant_from_name.rb +34 -0
  36. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +31 -47
  37. data/test/hexapdf/type/acro_form/test_field.rb +11 -0
  38. data/test/hexapdf/type/acro_form/test_form.rb +33 -0
  39. data/test/hexapdf/type/acro_form/test_java_script_actions.rb +226 -0
  40. data/test/hexapdf/type/acro_form/test_text_field.rb +44 -0
  41. metadata +7 -2
@@ -0,0 +1,498 @@
1
+ # -*- encoding: utf-8; frozen_string_literal: true -*-
2
+ #
3
+ #--
4
+ # This file is part of HexaPDF.
5
+ #
6
+ # HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
7
+ # Copyright (C) 2014-2024 Thomas Leitner
8
+ #
9
+ # HexaPDF is free software: you can redistribute it and/or modify it
10
+ # under the terms of the GNU Affero General Public License version 3 as
11
+ # published by the Free Software Foundation with the addition of the
12
+ # following permission added to Section 15 as permitted in Section 7(a):
13
+ # FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
14
+ # THOMAS LEITNER, THOMAS LEITNER DISCLAIMS THE WARRANTY OF NON
15
+ # INFRINGEMENT OF THIRD PARTY RIGHTS.
16
+ #
17
+ # HexaPDF is distributed in the hope that it will be useful, but WITHOUT
18
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
19
+ # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
20
+ # License for more details.
21
+ #
22
+ # You should have received a copy of the GNU Affero General Public License
23
+ # along with HexaPDF. If not, see <http://www.gnu.org/licenses/>.
24
+ #
25
+ # The interactive user interfaces in modified source and object code
26
+ # versions of HexaPDF must display Appropriate Legal Notices, as required
27
+ # under Section 5 of the GNU Affero General Public License version 3.
28
+ #
29
+ # In accordance with Section 7(b) of the GNU Affero General Public
30
+ # License, a covered work must retain the producer line in every PDF that
31
+ # is created or manipulated using HexaPDF.
32
+ #
33
+ # If the GNU Affero General Public License doesn't fit your need,
34
+ # commercial licenses are available at <https://gettalong.at/hexapdf/>.
35
+ #++
36
+
37
+ require 'json'
38
+ require 'hexapdf/error'
39
+ require 'hexapdf/layout/style'
40
+ require 'hexapdf/layout/text_fragment'
41
+ require 'hexapdf/layout/text_layouter'
42
+
43
+ module HexaPDF
44
+ module Type
45
+ module AcroForm
46
+
47
+ # The JavaScriptActions module implements JavaScript actions that can be specified for form
48
+ # fields, such as formatting or calculating a field's value.
49
+ #
50
+ # These JavaScript functions are not specified in the PDF specification but can be found in
51
+ # other reference materials (e.g. from Adobe).
52
+ #
53
+ # Formatting a field's value::
54
+ #
55
+ # The main entry point is #apply_format which applies the format to a value. Supported
56
+ # JavaScript actions are:
57
+ #
58
+ # * +AFNumber_Format+: See #af_number_format_action and #apply_af_number_format
59
+ #
60
+ # Calculating a field's value::
61
+ #
62
+ # The main entry point is #calculate which calculates a field's value. Supported JavaScript
63
+ # actions are:
64
+ #
65
+ # * +AFSimple_Calculate+: See #af_simple_calculate_action and #run_af_simple_calculate
66
+ # * Simplified Field Notation expressions: See #simplified_field_notation_action and
67
+ # #run_simplified_field_notation
68
+ #
69
+ # See: PDF2.0 s12.6.4.17
70
+ #
71
+ # See:
72
+ # - https://experienceleague.adobe.com/docs/experience-manager-learn/assets/FormsAPIReference.pdf
73
+ # - https://opensource.adobe.com/dc-acrobat-sdk-docs/library/jsapiref/JS_API_AcroJS.html#printf
74
+ module JavaScriptActions
75
+
76
+ # Implements a parser for the simplified field notation used for calculating field values.
77
+ #
78
+ # This notation is used if the predefined functions are too simple but the calculation can
79
+ # still be done by simple arithmetic.
80
+ class SimplifiedFieldNotationParser
81
+
82
+ # Raised if there was an error during parsing.
83
+ class ParseError < StandardError; end
84
+
85
+ # Creates a new instance for the given AcroForm +form+ instance and simplified field
86
+ # notation string +sfn_string+.
87
+ def initialize(form, sfn_string)
88
+ @form = form
89
+ @tokens = sfn_string.scan(/\p{Alpha}[^()*\/+-]*|\d+(?:[.,]\d*)?|[()*\/+-]/)
90
+ end
91
+
92
+ # Parses the string holding the simplified field notation.
93
+ #
94
+ # If +operations+ is :calculate, the calculation is performed and the result returned. If
95
+ # +operations+ is :generate, a JavaScript representation is generated and returned.
96
+ #
97
+ # +nil+ is returned regardless of the +operations+ value if there was any problem.
98
+ def parse(operations = :calculate)
99
+ operations = (operations == :calculate ? CALCULATE_OPERATIONS : JS_GENERATE_OPERATIONS)
100
+ result = expression(operations)
101
+ @tokens.empty? ? result : nil
102
+ rescue ParseError
103
+ nil
104
+ end
105
+
106
+ private
107
+
108
+ # Implementation of the operations for calculating the result.
109
+ CALCULATE_OPERATIONS = {
110
+ '+' => lambda {|l, r| l + r },
111
+ '-' => lambda {|l, r| l - r },
112
+ '*' => lambda {|l, r| l * r },
113
+ '/' => lambda {|l, r| l / r },
114
+ field: lambda {|field| JavaScriptActions.af_make_number(field.field_value) },
115
+ number: lambda {|token| JavaScriptActions.af_make_number(token) },
116
+ parens: lambda {|expr| expr },
117
+ }
118
+
119
+ # Implementation of the operations for generating the equivalent JavaScript code.
120
+ JS_GENERATE_OPERATIONS = {
121
+ '+' => lambda {|l, r| "#{l} + #{r}" },
122
+ '-' => lambda {|l, r| "#{l} - #{r}" },
123
+ '*' => lambda {|l, r| "#{l} * #{r}" },
124
+ '/' => lambda {|l, r| "#{l} / #{r}" },
125
+ field: lambda {|field| "AFMakeNumber(getField(#{field.full_field_name.to_json}).value)" },
126
+ number: lambda {|token| JavaScriptActions.af_make_number(token).to_s },
127
+ parens: lambda {|expr| "(#{expr})" },
128
+ }
129
+
130
+ # Parses the expression at the current position.
131
+ #
132
+ # expression = term [('+'|'-') term]*
133
+ def expression(operations)
134
+ result = term(operations)
135
+ while @tokens.first == '+' || @tokens.first == '-'
136
+ result = operations[@tokens.shift].call(result, term(operations))
137
+ end
138
+ result
139
+ end
140
+
141
+ # Parses the term at the current position.
142
+ #
143
+ # term = factor [('*'|'/') factor]*
144
+ def term(operations)
145
+ result = factor(operations)
146
+ while @tokens.first == '*' || @tokens.first == '/'
147
+ result = operations[@tokens.shift].call(result, factor(operations))
148
+ end
149
+ result
150
+ end
151
+
152
+ # Parses the factor at the current position.
153
+ #
154
+ # factor = '(' expr ')' | field_name | number
155
+ def factor(operations)
156
+ token = @tokens.shift
157
+ if token == '('
158
+ value = expression(operations)
159
+ raise ParseError, "Unmatched parentheses" unless @tokens.shift == ')'
160
+ operations[:parens].call(value)
161
+ elsif (field = @form.field_by_name(token.strip.gsub('\\', ''))) && field.terminal_field?
162
+ operations[:field].call(field)
163
+ elsif token.match?(/\A\d+(?:[.,]\d*)?\z/)
164
+ operations[:number].call(token)
165
+ else
166
+ raise ParseError, "Invalid token encountered: #{token}"
167
+ end
168
+ end
169
+
170
+ end
171
+
172
+ module_function
173
+
174
+ # Handles JavaScript field format actions for single-line text fields.
175
+ #
176
+ # The argument +value+ is the value that should be formatted and +format_action+ is the PDF
177
+ # format action object that should be applied. The latter may be +nil+ if no associated
178
+ # format action is available.
179
+ #
180
+ # Returns [value, nil_or_text_color] where value is the new, potentially changed field value
181
+ # and the second argument is either +nil+ (no change in color) or the color that should be
182
+ # used for the text value.
183
+ def apply_format(value, format_action)
184
+ return [value, nil] unless (action_string = action_string(format_action))
185
+ if action_string.start_with?('AFNumber_Format(')
186
+ apply_af_number_format(value, action_string)
187
+ else
188
+ [value, nil]
189
+ end
190
+ end
191
+
192
+ AF_NUMBER_FORMAT_MAPPINGS = { #:nodoc:
193
+ separator: {
194
+ point: 0,
195
+ point_no_thousands: 1,
196
+ comma: 2,
197
+ comma_no_thousands: 3,
198
+ },
199
+ negative: {
200
+ minus_black: 0,
201
+ red: 1,
202
+ parens_black: 2,
203
+ parens_red: 3,
204
+ },
205
+ }
206
+
207
+ # Returns the appropriate JavaScript action string for the AFNumber_Format function.
208
+ #
209
+ # +decimals+::
210
+ # The number of decimal digits to use. Default 2.
211
+ #
212
+ # +separator_style+::
213
+ # Specifies the character for the decimal and thousands separator, one of:
214
+ #
215
+ # :point:: (Default) Use point as decimal separator and comma as thousands separator.
216
+ # :point_no_thousands:: Use point as decimal separator and no thousands separator.
217
+ # :comma:: Use comma as decimal separator and point as thousands separator.
218
+ # :comma_no_thousands:: Use comma as decimal separator and no thousands separator.
219
+ #
220
+ # +negative_style+::
221
+ # Specifies how negative numbers should be formatted, one of:
222
+ #
223
+ # :minus_black:: (Default) Use minus before the number and black as color.
224
+ # :red:: Just use red as color.
225
+ # :parens_black:: Use parentheses around the number and black as color.
226
+ # :parens_red:: Use parentheses around the number and red as color.
227
+ #
228
+ # +currency_string+::
229
+ # Specifies the currency string that should be used. Default is the empty string.
230
+ #
231
+ # +prepend_currency+::
232
+ # Specifies whether the currency string should be prepended (+true+, default) or
233
+ # appended (+false).
234
+ #
235
+ # See: #apply_af_number_format
236
+ def af_number_format_action(decimals: 2, separator_style: :point, negative_style: :minus_black,
237
+ currency_string: "", prepend_currency: true)
238
+ "AFNumber_Format(#{decimals}, #{AF_NUMBER_FORMAT_MAPPINGS[:separator][separator_style]}, " \
239
+ "#{AF_NUMBER_FORMAT_MAPPINGS[:negative][negative_style]}, 0, \"#{currency_string}\", " \
240
+ "#{prepend_currency});"
241
+ end
242
+
243
+ # Regular expression for matching the AFNumber_Format method.
244
+ #
245
+ # See: #apply_af_number_format
246
+ AF_NUMBER_FORMAT_RE = /
247
+ \AAFNumber_Format\(
248
+ \s*(?<ndec>\d+)\s*,
249
+ \s*(?<sep_style>[0-3])\s*,
250
+ \s*(?<neg_style>[0-3])\s*,
251
+ \s*0\s*,
252
+ \s*(?<currency_string>".*?")\s*,
253
+ \s*(?<prepend>false|true)\s*
254
+ \);?\z
255
+ /x
256
+
257
+ # Implements the JavaScript AFNumber_Format function and returns the formatted field value.
258
+ #
259
+ # The argument +value+ has to be the field's value (a String) and +action_string+ has to be
260
+ # the JavaScript action string.
261
+ #
262
+ # The AFNumber_Format function assumes that the text field's value contains a number (as a
263
+ # string) and formats it according to the instructions.
264
+ #
265
+ # It has the form <tt>AFNumber_Format(no_of_decimals, separator_style, negative_style,
266
+ # currency_style, currency_string, prepend_currency)</tt> where the arguments have the
267
+ # following meaning:
268
+ #
269
+ # +no_of_decimals+::
270
+ # The number of decimal places after the decimal point, e.g. for 3 it would result in
271
+ # 123.456.
272
+ #
273
+ # +separator_style+::
274
+ # Defines which decimal separator and whether a thousands separator should be used.
275
+ #
276
+ # Possible values are:
277
+ #
278
+ # +0+:: Comma for thousands separator, point for decimal separator: 12,345.67
279
+ # +1+:: No thousands separator, point for decimal separator: 12345.67
280
+ # +2+:: Point for thousands separator, comma for decimal separator: 12.345,67
281
+ # +3+:: No thousands separator, comma for decimal separator: 12345,67
282
+ #
283
+ # +negative_style+::
284
+ # Defines how negative numbers should be formatted.
285
+ #
286
+ # Possible values are:
287
+ #
288
+ # +0+:: With minus and in color black: -12,345.67
289
+ # +1+:: Just in color red: 12,345.67
290
+ # +2+:: With parentheses and in color black: (12,345.67)
291
+ # +3+:: With parentheses and in color red: (12,345.67)
292
+ #
293
+ # +currency_style+::
294
+ # This argument is not used, should be 0.
295
+ #
296
+ # +currency_string+::
297
+ # A string with the currency symbol, e.g. € or $.
298
+ #
299
+ # +prepend_currency+::
300
+ # A boolean defining whether the currency string should be prepended (+true+) or appended
301
+ # (+false+).
302
+ def apply_af_number_format(value, action_string)
303
+ return [value, nil] unless (match = AF_NUMBER_FORMAT_RE.match(action_string))
304
+ value = af_make_number(value)
305
+ format = "%.#{match[:ndec]}f"
306
+ text_color = 'black'
307
+
308
+ currency_string = JSON.parse(match[:currency_string])
309
+ format = (match[:prepend] == 'true' ? currency_string + format : format + currency_string)
310
+
311
+ if value < 0
312
+ value = value.abs
313
+ case match[:neg_style]
314
+ when '0' # MinusBlack
315
+ format = "-#{format}"
316
+ when '1' # Red
317
+ text_color = 'red'
318
+ when '2' # ParensBlack
319
+ format = "(#{format})"
320
+ when '3' # ParensRed
321
+ format = "(#{format})"
322
+ text_color = 'red'
323
+ end
324
+ end
325
+
326
+ result = sprintf(format, value)
327
+
328
+ before_decimal_point, after_decimal_point = result.split('.')
329
+ if match[:sep_style] == '0' || match[:sep_style] == '2'
330
+ separator = (match[:sep_style] == '0' ? ',' : '.')
331
+ before_decimal_point.gsub!(/\B(?=(\d\d\d)+(?:[^\d]|\z))/, separator)
332
+ end
333
+ result = if after_decimal_point
334
+ decimal_point = (match[:sep_style] =~ /[01]/ ? '.' : ',')
335
+ "#{before_decimal_point}#{decimal_point}#{after_decimal_point}"
336
+ else
337
+ before_decimal_point
338
+ end
339
+
340
+ [result, text_color]
341
+ end
342
+
343
+ # Handles JavaScript calculate actions for single-line text fields.
344
+ #
345
+ # The argument +form+ is the main Form instance of the document (needed for accessing the
346
+ # fields for the calculation) and +calculation_action+ is the PDF calculate action object
347
+ # that should be applied.
348
+ #
349
+ # Returns the calculated value as string if the calculation was succcessful or +nil+
350
+ # otherwise.
351
+ #
352
+ # A calculation may not be successful if
353
+ #
354
+ # * HexaPDF doesn't support the specific calculate action (e.g. because it contains general
355
+ # JavaScript instructions), or if
356
+ # * there was an error during the calculation (e.g. because a field could not be resolved).
357
+ def calculate(form, calculate_action)
358
+ return nil unless (action_string = action_string(calculate_action))
359
+ result = if action_string.start_with?('AFSimple_Calculate(')
360
+ run_af_simple_calculate(form, action_string)
361
+ elsif action_string.match?(/\/\*\*\s*BVCALC/)
362
+ run_simplified_field_notation(form, action_string)
363
+ else
364
+ nil
365
+ end
366
+ result && (result == result.truncate ? result.to_i.to_s : result.to_s)
367
+ end
368
+
369
+ AF_SIMPLE_CALCULATE_MAPPING = { #:nodoc:
370
+ sum: 'SUM',
371
+ average: 'AVG',
372
+ product: 'PRD',
373
+ min: 'MIN',
374
+ max: 'MAX',
375
+ }
376
+
377
+ # Returns the appropriate JavaScript action string for the AFSimple_Calculate function.
378
+ #
379
+ # +type+::
380
+ # The type of operation that should be used, one of:
381
+ #
382
+ # :sum:: Sums the values of the given +fields+.
383
+ # :average:: Calculates the average value of the given +fields+.
384
+ # :product:: Multiplies the values of the given +fields+.
385
+ # :min:: Uses the minimum value of the given +fields+.
386
+ # :max:: Uses the maximum value of the given +fields+.
387
+ #
388
+ # +fields+::
389
+ # An array of form field objects and/or full field names.
390
+ #
391
+ # See: #run_af_simple_calculate
392
+ def af_simple_calculate_action(type, fields)
393
+ fields = fields.map {|field| field.kind_of?(String) ? field : field.full_field_name }
394
+ "AFSimple_Calculate(\"#{AF_SIMPLE_CALCULATE_MAPPING[type]}\", #{fields.to_json});"
395
+ end
396
+
397
+ # Regular expression for matching the AFSimple_Calculate function.
398
+ #
399
+ # See: #run_af_simple_calculate
400
+ AF_SIMPLE_CALCULATE_RE = /
401
+ \AAFSimple_Calculate\(
402
+ \s*"(?<function>AVG|SUM|PRD|MIN|MAX)"\s*,
403
+ \s*(?<fields>.*)\s*
404
+ \);?\z
405
+ /x
406
+
407
+ # Mapping of AFSimple_Calculate function names to implementations.
408
+ #
409
+ # See: #run_af_simple_calculate
410
+ AF_SIMPLE_CALCULATE = {
411
+ 'AVG' => lambda {|values| values.sum / values.length },
412
+ 'SUM' => lambda {|values| values.sum },
413
+ 'PRD' => lambda {|values| values.inject {|product, val| product * val } },
414
+ 'MIN' => lambda {|values| values.min },
415
+ 'MAX' => lambda {|values| values.max },
416
+ }
417
+
418
+ # Implements the JavaScript AFSimple_Calculate function and returns the calculated value.
419
+ #
420
+ # The argument +form+ has to be the document's main AcroForm object and +action_string+ has
421
+ # to be the JavaScript action string.
422
+ #
423
+ # The AFSimple_Calculate function applies one of several predefined functions to the values
424
+ # of the given fields. The values of those fields need to be strings representing numbers.
425
+ #
426
+ # It has the form <tt>AFSimple_Calculate(function, fields))</tt> where the arguments have
427
+ # the following meaning:
428
+ #
429
+ # +function+::
430
+ # The name of the calculation function that should be applied to the values.
431
+ #
432
+ # Possible values are:
433
+ #
434
+ # +SUM+:: Calculate the sum of the given field values.
435
+ # +AVG+:: Calculate the average of the given field values.
436
+ # +PRD+:: Calculate the product of the given field values.
437
+ # +MIN+:: Calculate the minimum of the given field values.
438
+ # +MAX+:: Calculate the maximum of the given field values.
439
+ #
440
+ # +fields+::
441
+ # An array of AcroForm field names the values of which should be used.
442
+ def run_af_simple_calculate(form, action_string)
443
+ return nil unless (match = AF_SIMPLE_CALCULATE_RE.match(action_string))
444
+ function = match[:function]
445
+ values = match[:fields].scan(/".*?"/).map do |name|
446
+ return nil unless (field = form.field_by_name(name[1..-2]))
447
+ af_make_number(field.field_value)
448
+ end
449
+ AF_SIMPLE_CALCULATE.fetch(function)&.call(values)
450
+ end
451
+
452
+ # Returns the appropriate JavaScript action string for a calculate action that uses
453
+ # Simplified Field Notation.
454
+ #
455
+ # The argument +form+ has to be the document's main AcroForm object and +sfn_string+ the
456
+ # string containing the simplified field notation.
457
+ #
458
+ # See: #run_simplified_field_notation
459
+ def simplified_field_notation_action(form, sfn_string)
460
+ js_part = SimplifiedFieldNotationParser.new(form, sfn_string).parse(:generate)
461
+ raise ArgumentError, "Invalid simplified field notation rule" unless js_part
462
+ "/** BVCALC #{sfn_string} EVCALC **/ event.value = #{js_part}"
463
+ end
464
+
465
+ # Implements parsing of the simplified field notation (SFN).
466
+ #
467
+ # The argument +form+ has to be the document's main AcroForm object and +action_string+ has
468
+ # to be the JavaScript action string.
469
+ #
470
+ # This notation is more powerful than AFSimple_Calculate as it allows arbitrary expressions
471
+ # consisting of additions, substractions, multiplications and divisions, possibly grouped
472
+ # using parentheses, and field names (which stand in for their value) as well as numbers.
473
+ #
474
+ # Note: The implementation has been created by looking at sample documents using SFN. As
475
+ # such this may not work for all documents that use SFN.
476
+ def run_simplified_field_notation(form, action_string)
477
+ return nil unless (match = /BVCALC(.*?)EVCALC/m.match(action_string))
478
+ SimplifiedFieldNotationParser.new(form, match[1]).parse
479
+ end
480
+
481
+ # Returns the numeric value of the string, interpreting comma as point.
482
+ def af_make_number(value)
483
+ value.to_s.tr(',', '.').to_f
484
+ end
485
+
486
+ # Returns the JavaScript action string for the given action.
487
+ def action_string(action)
488
+ return nil unless action && action[:S] == :JavaScript
489
+ result = action[:JS]
490
+ result.kind_of?(HexaPDF::Stream) ? result.stream : result
491
+ end
492
+ private :action_string
493
+
494
+ end
495
+
496
+ end
497
+ end
498
+ end
@@ -36,6 +36,7 @@
36
36
 
37
37
  require 'hexapdf/error'
38
38
  require 'hexapdf/type/acro_form/variable_text_field'
39
+ require 'hexapdf/type/acro_form/java_script_actions'
39
40
 
40
41
  module HexaPDF
41
42
  module Type
@@ -168,6 +169,8 @@ module HexaPDF
168
169
  raise HexaPDF::Error, "Storing a field value for a password field is not allowed"
169
170
  elsif comb_text_field? && !key?(:MaxLen)
170
171
  raise HexaPDF::Error, "A comb text field need a valid /MaxLen value"
172
+ elsif str && !str.kind_of?(String)
173
+ @document.config['acro_form.on_invalid_value'].call(self, str)
171
174
  end
172
175
  str = str.gsub(/[[:space:]]/, ' ') if str && concrete_field_type == :single_line_text_field
173
176
  if key?(:MaxLen) && str && str.length > self[:MaxLen]
@@ -241,6 +244,81 @@ module HexaPDF
241
244
  create_appearances(force: true)
242
245
  end
243
246
 
247
+ # Sets the specified JavaScript format action on the field's widgets.
248
+ #
249
+ # This action is executed when the field value needs to be formatted for rendering in the
250
+ # appearance streams of the associated widgets.
251
+ #
252
+ # The argument +type+ can be one of the following:
253
+ #
254
+ # :number::
255
+ # Assumes that the field value is a number and formats it according to the given
256
+ # arguments. See JavaScriptActions.af_number_format_action for details on the arguments.
257
+ def set_format_action(type, **arguments)
258
+ action_string = case type
259
+ when :number then JavaScriptActions.af_number_format_action(**arguments)
260
+ else
261
+ raise ArgumentError, "Invalid value for type argument: #{type.inspect}"
262
+ end
263
+ self[:AA] ||= {}
264
+ self[:AA][:F] = {S: :JavaScript, JS: action_string}
265
+ end
266
+
267
+ # Sets the specified JavaScript calculate action on the field.
268
+ #
269
+ # This action is executed by a viewer when any field's value changes so as to recalculate
270
+ # the value of this field. Usually, the field is also flagged as read only to avoid a user
271
+ # changing the value manually.
272
+ #
273
+ # Note that HexaPDF *doesn't* automatically recalculate field values, use
274
+ # Form#recalculate_fields to manually kick off recalculation.
275
+ #
276
+ # The argument +type+ can be one of the following:
277
+ #
278
+ # :sum::
279
+ # Sums the values of the given +fields+.
280
+ #
281
+ # :average::
282
+ # Calculates the average value of the given +fields+.
283
+ #
284
+ # :product::
285
+ # Multiplies the values of the given +fields+.
286
+ #
287
+ # :min::
288
+ # Uses the minimum value of the given +fields+.
289
+ #
290
+ # :max::
291
+ # Uses the maximum value of the given +fields+.
292
+ #
293
+ # :sfn::
294
+ # Uses the Simplified Field Notation for calculating the field's value. This allows for
295
+ # more complex calculations involving addition, subtraction, multiplication and
296
+ # division. Field values are specified by using the full field names which should not
297
+ # contain spaces or punctuation characters except point.
298
+ #
299
+ # The +fields+ argument needs to contain a string with the SFN calculation rule.
300
+ #
301
+ # Here are some examples:
302
+ #
303
+ # field1 + field2 - field3
304
+ # (field.1 + field.2) * (field.3 - field.4)
305
+ #
306
+ # Note: Setting this action appends the field to the main Form's /CO entry which specifies
307
+ # the calculation order. Rearrange the entries as needed.
308
+ def set_calculate_action(type, fields: nil)
309
+ action_string = case type
310
+ when :sum, :average, :product, :min, :max
311
+ JavaScriptActions.af_simple_calculate_action(type, fields)
312
+ when :sfn
313
+ JavaScriptActions.simplified_field_notation_action(document.acro_form, fields)
314
+ else
315
+ raise ArgumentError, "Invalid value for type argument: #{type.inspect}"
316
+ end
317
+ self[:AA] ||= {}
318
+ self[:AA][:C] = {S: :JavaScript, JS: action_string}
319
+ (document.acro_form[:CO] ||= []) << self
320
+ end
321
+
244
322
  private
245
323
 
246
324
  def perform_validation #:nodoc:
@@ -51,6 +51,7 @@ module HexaPDF
51
51
  autoload(:SignatureField, 'hexapdf/type/acro_form/signature_field')
52
52
 
53
53
  autoload(:AppearanceGenerator, 'hexapdf/type/acro_form/appearance_generator')
54
+ autoload(:JavaScriptActions, 'hexapdf/type/acro_form/java_script_actions')
54
55
 
55
56
  end
56
57
 
@@ -85,7 +85,7 @@ module HexaPDF
85
85
  define_field :BS, type: :Border, version: '1.2'
86
86
  define_field :Parent, type: Dictionary
87
87
 
88
- # Returs the AcroForm field object to which this widget annotation belongs.
88
+ # Returns the AcroForm field object to which this widget annotation belongs.
89
89
  #
90
90
  # Since a widget and a field can share the same dictionary object, the returned object is
91
91
  # often just the widget re-wrapped in the correct field class.
@@ -37,6 +37,6 @@
37
37
  module HexaPDF
38
38
 
39
39
  # The version of HexaPDF.
40
- VERSION = '0.40.0'
40
+ VERSION = '0.41.0'
41
41
 
42
42
  end
@@ -17,7 +17,6 @@ describe HexaPDF::Encryption::AES do
17
17
  end
18
18
 
19
19
  def process(data)
20
- raise "invalid data" if data.empty? || data.length % 16 != 0
21
20
  data
22
21
  end
23
22
  end
@@ -56,10 +55,17 @@ describe HexaPDF::Encryption::AES do
56
55
  assert_equal('', @algorithm_class.decrypt('some key' * 2, 'iv' * 8))
57
56
  end
58
57
 
59
- it "fails on decryption if not enough bytes are provided" do
58
+ it "handles invalid files where not enough bytes are provided" do
59
+ @algorithm_class.decrypt('some' * 4, 'a' * 36) do |msg|
60
+ assert_match(/32 \+ 16/, msg)
61
+ false
62
+ end
60
63
  assert_raises(HexaPDF::EncryptionError) do
61
64
  @algorithm_class.decrypt('some' * 4, 'no iv')
62
65
  end
66
+ assert_raises(HexaPDF::EncryptionError) do
67
+ @algorithm_class.decrypt('some' * 4, 'no iv') { true }
68
+ end
63
69
  end
64
70
  end
65
71
 
@@ -126,12 +132,16 @@ describe HexaPDF::Encryption::AES do
126
132
  assert_equal('', collector(@algorithm_class.decryption_fiber('key', Fiber.new { '' })))
127
133
  end
128
134
 
129
- it "fails on decryption if not enough bytes are provided" do
130
- [4, 20, 40].each do |length|
131
- assert_raises(HexaPDF::EncryptionError) do
132
- collector(@algorithm_class.decryption_fiber('some' * 4,
133
- Fiber.new { 'a' * length }))
134
- end
135
+ it "handles invalid files where not enough bytes are provided" do
136
+ collector(@algorithm_class.decryption_fiber('some' * 4, Fiber.new { 'a' * 40 }) do |msg|
137
+ assert_match(/32 \+ 16/, msg)
138
+ false
139
+ end)
140
+ assert_raises(HexaPDF::EncryptionError) do
141
+ collector(@algorithm_class.decryption_fiber('some' * 4, Fiber.new { 'a' * 40 }))
142
+ end
143
+ assert_raises(HexaPDF::EncryptionError) do
144
+ collector(@algorithm_class.decryption_fiber('some' * 4, Fiber.new { 'a' * 40 })) { true }
135
145
  end
136
146
  end
137
147
  end