hexapdf 0.39.1 → 0.41.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +59 -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/list_box.rb +40 -33
  21. data/lib/hexapdf/layout/style.rb +25 -23
  22. data/lib/hexapdf/layout/text_box.rb +3 -3
  23. data/lib/hexapdf/type/acro_form/appearance_generator.rb +11 -76
  24. data/lib/hexapdf/type/acro_form/field.rb +14 -0
  25. data/lib/hexapdf/type/acro_form/form.rb +25 -8
  26. data/lib/hexapdf/type/acro_form/java_script_actions.rb +498 -0
  27. data/lib/hexapdf/type/acro_form/text_field.rb +78 -0
  28. data/lib/hexapdf/type/acro_form.rb +1 -0
  29. data/lib/hexapdf/type/annotations/widget.rb +1 -1
  30. data/lib/hexapdf/version.rb +1 -1
  31. data/test/hexapdf/encryption/test_aes.rb +18 -8
  32. data/test/hexapdf/encryption/test_security_handler.rb +17 -0
  33. data/test/hexapdf/encryption/test_standard_security_handler.rb +1 -1
  34. data/test/hexapdf/font/cmap/test_parser.rb +5 -3
  35. data/test/hexapdf/font/test_cmap.rb +8 -0
  36. data/test/hexapdf/font_loader/test_from_configuration.rb +4 -0
  37. data/test/hexapdf/font_loader/test_variant_from_name.rb +34 -0
  38. data/test/hexapdf/layout/test_list_box.rb +30 -1
  39. data/test/hexapdf/layout/test_style.rb +1 -1
  40. data/test/hexapdf/layout/test_text_box.rb +4 -4
  41. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +31 -47
  42. data/test/hexapdf/type/acro_form/test_field.rb +11 -0
  43. data/test/hexapdf/type/acro_form/test_form.rb +33 -0
  44. data/test/hexapdf/type/acro_form/test_java_script_actions.rb +226 -0
  45. data/test/hexapdf/type/acro_form/test_text_field.rb +44 -0
  46. 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.39.1'
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