Module: HexaPDF::Type::AcroForm::JavaScriptActions

Defined in:
lib/hexapdf/type/acro_form/java_script_actions.rb

Overview

The JavaScriptActions module implements JavaScript actions that can be specified for form fields, such as formatting or calculating a field’s value.

These JavaScript functions are not specified in the PDF specification but can be found in other reference materials (e.g. from Adobe).

Formatting a field’s value

The main entry point is #apply_format which applies the format to a value. Supported JavaScript actions are:

  • AFNumber_Format: See #af_number_format_action and #apply_af_number_format

  • AFPercent_Format: See #af_percent_format_action and #apply_af_percent_format

Calculating a field’s value

The main entry point is #calculate which calculates a field’s value. Supported JavaScript actions are:

  • AFSimple_Calculate: See #af_simple_calculate_action and #run_af_simple_calculate

  • Simplified Field Notation expressions: See #simplified_field_notation_action and #run_simplified_field_notation

See: PDF2.0 s12.6.4.17

See:

Defined Under Namespace

Classes: SimplifiedFieldNotationParser

Constant Summary collapse

AF_NUMBER_FORMAT_MAPPINGS =

:nodoc:

{ #:nodoc:
  separator: {
    point: 0,
    point_no_thousands: 1,
    comma: 2,
    comma_no_thousands: 3,
  },
  negative: {
    minus_black: 0,
    red: 1,
    parens_black: 2,
    parens_red: 3,
  },
}
AF_NUMBER_FORMAT_RE =

Regular expression for matching the AFNumber_Format method.

See: #apply_af_number_format

/
  \AAFNumber_Format\(
    \s*(?<ndec>\d+)\s*,
    \s*(?<sep_style>[0-3])\s*,
    \s*(?<neg_style>[0-3])\s*,
    \s*0\s*,
    \s*(?<currency_string>".*?")\s*,
    \s*(?<prepend>false|true)\s*
  \);?\z
/x
AF_PERCENT_FORMAT_RE =

Regular expression for matching the AFPercent_Format method.

See: #apply_af_percent_format

/
  \AAFPercent_Format\(
    \s*(?<ndec>\d+)\s*,
    \s*(?<sep_style>[0-3])\s*
  \);?\z
/x
AF_TIME_FORMAT_MAPPINGS =

:nodoc:

{ #:nodoc:
  format_integers: {
    hh_mm: 0,
    0 => 0,
    hh12_mm: 1,
    1 => 1,
    hh_mm_ss: 2,
    2 => 2,
    hh12_mm_ss: 3,
    3 => 3,
  },
  strftime_format: {
    '0' => '%H:%M',
    '1' => '%l:%M %p',
    '2' => '%H:%M:%S',
    '3' => '%l:%M:%S %p',
  },
}
AF_TIME_FORMAT_RE =

Regular expression for matching the AFTime_Format method.

See: #apply_af_time_format

/
  \AAFTime_Format\(
    \s*(?<time_format>[0-3])\s*
  \);?\z
/x
AF_SIMPLE_CALCULATE_MAPPING =

:nodoc:

{ #:nodoc:
  sum: 'SUM',
  average: 'AVG',
  product: 'PRD',
  min: 'MIN',
  max: 'MAX',
}
AF_SIMPLE_CALCULATE_RE =

Regular expression for matching the AFSimple_Calculate function.

See: #run_af_simple_calculate

/
  \AAFSimple_Calculate\(
    \s*"(?<function>AVG|SUM|PRD|MIN|MAX)"\s*,
    \s*(?<fields>.*)\s*
  \);?\z
/x
AF_SIMPLE_CALCULATE =

Mapping of AFSimple_Calculate function names to implementations.

See: #run_af_simple_calculate

{
  'AVG' => lambda {|values| values.sum / values.length },
  'SUM' => lambda {|values| values.sum },
  'PRD' => lambda {|values| values.inject {|product, val| product * val } },
  'MIN' => lambda {|values| values.min },
  'MAX' => lambda {|values| values.max },
}

Class Method Summary collapse

Class Method Details

.action_string(action) ⇒ Object

Returns the JavaScript action string for the given action.



638
639
640
641
642
# File 'lib/hexapdf/type/acro_form/java_script_actions.rb', line 638

def action_string(action)
  return nil unless action && action[:S] == :JavaScript
  result = action[:JS]
  result.kind_of?(HexaPDF::Stream) ? result.stream : result
end

.af_format_number(value, format, sep_style) ⇒ Object

Formats the numeric value according to the format string and separator style.



620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
# File 'lib/hexapdf/type/acro_form/java_script_actions.rb', line 620

def af_format_number(value, format, sep_style)
  result = sprintf(format, value)

  before_decimal_point, after_decimal_point = result.split('.')
  if sep_style == '0' || sep_style == '2'
    separator = (sep_style == '0' ? ',' : '.')
    before_decimal_point.gsub!(/\B(?=(\d\d\d)+(?:[^\d]|\z))/, separator)
  end

  if after_decimal_point
    decimal_point = (sep_style <= "1" ? '.' : ',')
    "#{before_decimal_point}#{decimal_point}#{after_decimal_point}"
  else
    before_decimal_point
  end
end

.af_make_number(value) ⇒ Object

Returns the numeric value of the string, interpreting comma as point.



615
616
617
# File 'lib/hexapdf/type/acro_form/java_script_actions.rb', line 615

def af_make_number(value)
  value.to_s.tr(',', '.').to_f
end

.af_number_format_action(decimals: 2, separator_style: :point, negative_style: :minus_black, currency_string: "", prepend_currency: true) ⇒ Object

Returns the appropriate JavaScript action string for the AFNumber_Format function.

decimals

The number of decimal digits to use. Default 2.

separator_style

Specifies the character for the decimal and thousands separator, one of:

:point

(Default) Use point as decimal separator and comma as thousands separator.

:point_no_thousands

Use point as decimal separator and no thousands separator.

:comma

Use comma as decimal separator and point as thousands separator.

:comma_no_thousands

Use comma as decimal separator and no thousands separator.

negative_style

Specifies how negative numbers should be formatted, one of:

:minus_black

(Default) Use minus before the number and black as color.

:red

Just use red as color.

:parens_black

Use parentheses around the number and black as color.

:parens_red

Use parentheses around the number and red as color.

currency_string

Specifies the currency string that should be used. Default is the empty string.

prepend_currency

Specifies whether the currency string should be prepended (true, default) or appended (+false).

See: #apply_af_number_format



242
243
244
245
246
247
248
249
250
251
252
253
254
# File 'lib/hexapdf/type/acro_form/java_script_actions.rb', line 242

def af_number_format_action(decimals: 2, separator_style: :point, negative_style: :minus_black,
                            currency_string: "", prepend_currency: true)
  separator_style = AF_NUMBER_FORMAT_MAPPINGS[:separator].fetch(separator_style) do
    raise ArgumentError, "Unsupported value for separator_style argument: #{separator_style}"
  end
  negative_style = AF_NUMBER_FORMAT_MAPPINGS[:negative].fetch(negative_style) do
    raise ArgumentError, "Unsupported value for negative_style argument: #{negative_style}"
  end

  "AFNumber_Format(#{decimals}, #{separator_style}, " \
    "#{negative_style}, 0, \"#{currency_string}\", " \
    "#{prepend_currency});"
end

.af_percent_format_action(decimals: 2, separator_style: :point) ⇒ Object

Returns the appropriate JavaScript action string for the AFPercent_Format function.

decimals

The number of decimal digits to use. Default 2.

separator_style

Specifies the character for the decimal and thousands separator, one of:

:point

(Default) Use point as decimal separator and comma as thousands separator.

:point_no_thousands

Use point as decimal separator and no thousands separator.

:comma

Use comma as decimal separator and point as thousands separator.

:comma_no_thousands

Use comma as decimal separator and no thousands separator.

See: #apply_af_percent_format



356
357
358
359
360
361
# File 'lib/hexapdf/type/acro_form/java_script_actions.rb', line 356

def af_percent_format_action(decimals: 2, separator_style: :point)
  separator_style = AF_NUMBER_FORMAT_MAPPINGS[:separator].fetch(separator_style) do
    raise ArgumentError, "Unsupported value for separator_style argument: #{separator_style}"
  end
  "AFPercent_Format(#{decimals}, #{separator_style});"
end

.af_simple_calculate_action(type, fields) ⇒ Object

Returns the appropriate JavaScript action string for the AFSimple_Calculate function.

type

The type of operation that should be used, one of:

:sum

Sums the values of the given fields.

:average

Calculates the average value of the given fields.

:product

Multiplies the values of the given fields.

:min

Uses the minimum value of the given fields.

:max

Uses the maximum value of the given fields.

fields

An array of form field objects and/or full field names.

See: #run_af_simple_calculate



525
526
527
528
# File 'lib/hexapdf/type/acro_form/java_script_actions.rb', line 525

def af_simple_calculate_action(type, fields)
  fields = fields.map {|field| field.kind_of?(String) ? field : field.full_field_name }
  "AFSimple_Calculate(\"#{AF_SIMPLE_CALCULATE_MAPPING[type]}\", #{fields.to_json});"
end

.af_time_format_action(format: :hh_mm) ⇒ Object

Returns the appropriate JavaScript action string for the AFTime_Format function.

format

Specifies the time format, one of:

:hh_mm

(Default) Use 24h time format %H:%M (e.g. 15:25)

:hh12_mm

(Default) Use 12h time format %l:%M %p (e.g. 3:25 PM)

:hh_mm_ss

Use 24h time format with seconds %H:%M:%S (e.g. 15:25:37)

:hh12_mm_ss

Use 24h time format with seconds %l:%M:%S %p (e.g. 3:25:37 PM)

See: #apply_af_time_format



432
433
434
435
436
437
# File 'lib/hexapdf/type/acro_form/java_script_actions.rb', line 432

def af_time_format_action(format: :hh_mm)
  format = AF_TIME_FORMAT_MAPPINGS[:format_integers].fetch(format) do
    raise ArgumentError, "Unsupported value for time_format argument: #{format}"
  end
  "AFTime_Format(#{format});"
end

.apply_af_number_format(value, action_string) ⇒ Object

Implements the JavaScript AFNumber_Format function and returns the formatted field value.

The argument value has to be the field’s value (a String) and action_string has to be the JavaScript action string.

The AFNumber_Format function assumes that the text field’s value contains a number (as a string) and formats it according to the instructions.

It has the form AFNumber_Format(no_of_decimals, separator_style, negative_style, currency_style, currency_string, prepend_currency) where the arguments have the following meaning:

no_of_decimals

The number of decimal places after the decimal point, e.g. for 3 it would result in 123.456.

separator_style

Defines which decimal separator and whether a thousands separator should be used.

Possible values are:

0

Comma for thousands separator, point for decimal separator: 12,345.67

1

No thousands separator, point for decimal separator: 12345.67

2

Point for thousands separator, comma for decimal separator: 12.345,67

3

No thousands separator, comma for decimal separator: 12345,67

negative_style

Defines how negative numbers should be formatted.

Possible values are:

0

With minus and in color black: -12,345.67

1

Just in color red: 12,345.67

2

With parentheses and in color black: (12,345.67)

3

With parentheses and in color red: (12,345.67)

currency_style

This argument is not used, should be 0.

currency_string

A string with the currency symbol, e.g. € or $.

prepend_currency

A boolean defining whether the currency string should be prepended (true) or appended (false).



315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
# File 'lib/hexapdf/type/acro_form/java_script_actions.rb', line 315

def apply_af_number_format(value, action_string)
  return [value, nil] unless (match = AF_NUMBER_FORMAT_RE.match(action_string))
  value = af_make_number(value)
  format = "%.#{match[:ndec]}f"
  text_color = 'black'

  currency_string = JSON.parse(match[:currency_string])
  format = (match[:prepend] == 'true' ? currency_string + format : format + currency_string)

  if value < 0
    value = value.abs
    case match[:neg_style]
    when '0' # MinusBlack
      format = "-#{format}"
    when '1' # Red
      text_color = 'red'
    when '2' # ParensBlack
      format = "(#{format})"
    when '3' # ParensRed
      format = "(#{format})"
      text_color = 'red'
    end
  end

  [af_format_number(value, format, match[:sep_style]), text_color]
end

.apply_af_percent_format(value, action_string) ⇒ Object

Implements the JavaScript AFPercent_Format function and returns the formatted field value.

The argument value has to be the field’s value (a String) and action_string has to be the JavaScript action string.

The AFPercent_Format function assumes that the text field’s value contains a number (as a string) and formats it according to the instructions.

It has the form AFPercent_Format(no_of_decimals, separator_style) where the arguments have the following meaning:

no_of_decimals

The number of decimal places after the decimal point, e.g. for 3 it would result in 123.456.

separator_style

Defines which decimal separator and whether a thousands separator should be used.

Possible values are:

0

Comma for thousands separator, point for decimal separator: 12,345.67

1

No thousands separator, point for decimal separator: 12345.67

2

Point for thousands separator, comma for decimal separator: 12.345,67

3

No thousands separator, comma for decimal separator: 12345,67



397
398
399
400
# File 'lib/hexapdf/type/acro_form/java_script_actions.rb', line 397

def apply_af_percent_format(value, action_string)
  return value unless (match = AF_PERCENT_FORMAT_RE.match(action_string))
  af_format_number(af_make_number(value) * 100, "%.#{match[:ndec]}f%%", match[:sep_style])
end

.apply_af_time_format(value, action_string) ⇒ Object

Implements the JavaScript AFTime_Format function and returns the formatted field value.

The argument value has to be the field’s value (a String) and action_string has to be the JavaScript action string.

The AFTime_Format function assumes that the text field’s value contains a valid time string (for HexaPDF that is anything Time.parse can work with) and formats it according to the instructions.

It has the form AFTime_Format(time_format) where the argument has the following meaning:

time_format

Defines the time format which should be applied.

Possible values are:

0

Use 24h time format, e.g. 15:25

1

Use 12h time format, e.g. 3:25 PM

2

Use 24h time format with seconds, e.g. 15:25:37

3

Use 12h time format with seconds, e.g. 3:25:37 PM



469
470
471
472
473
474
# File 'lib/hexapdf/type/acro_form/java_script_actions.rb', line 469

def apply_af_time_format(value, action_string)
  return value unless (match = AF_TIME_FORMAT_RE.match(action_string))
  value = Time.parse(value) rescue nil
  return "" unless value
  value.strftime(AF_TIME_FORMAT_MAPPINGS[:strftime_format][match[:time_format]]).strip
end

.apply_format(value, format_action) ⇒ Object

Handles JavaScript field format actions for single-line text fields.

The argument value is the value that should be formatted and format_action is the PDF format action object that should be applied. The latter may be nil if no associated format action is available.

Returns [value, nil_or_text_color] where value is the new, potentially changed field value and the second argument is either nil (no change in color) or the color that should be used for the text value.



185
186
187
188
189
190
191
192
193
194
195
196
# File 'lib/hexapdf/type/acro_form/java_script_actions.rb', line 185

def apply_format(value, format_action)
  return [value, nil] unless (action_string = action_string(format_action))
  if action_string.start_with?('AFNumber_Format(')
    apply_af_number_format(value, action_string)
  elsif action_string.start_with?('AFPercent_Format(')
    apply_af_percent_format(value, action_string)
  elsif action_string.start_with?('AFTime_Format(')
    apply_af_time_format(value, action_string)
  else
    [value, nil]
  end
end

.calculate(form, calculate_action) ⇒ Object

Handles JavaScript calculate actions for single-line text fields.

The argument form is the main Form instance of the document (needed for accessing the fields for the calculation) and calculation_action is the PDF calculate action object that should be applied.

Returns the calculated value as string if the calculation was succcessful or nil otherwise.

A calculation may not be successful if

  • HexaPDF doesn’t support the specific calculate action (e.g. because it contains general JavaScript instructions), or if

  • there was an error during the calculation (e.g. because a field could not be resolved).



490
491
492
493
494
495
496
497
498
499
500
# File 'lib/hexapdf/type/acro_form/java_script_actions.rb', line 490

def calculate(form, calculate_action)
  return nil unless (action_string = action_string(calculate_action))
  result = if action_string.start_with?('AFSimple_Calculate(')
             run_af_simple_calculate(form, action_string)
           elsif action_string.match?(/\/\*\*\s*BVCALC/)
             run_simplified_field_notation(form, action_string)
           else
             nil
           end
  result && (result == result.truncate ? result.to_i.to_s : result.to_s)
end

.run_af_simple_calculate(form, action_string) ⇒ Object

Implements the JavaScript AFSimple_Calculate function and returns the calculated value.

The argument form has to be the document’s main AcroForm object and action_string has to be the JavaScript action string.

The AFSimple_Calculate function applies one of several predefined functions to the values of the given fields. The values of those fields need to be strings representing numbers.

It has the form AFSimple_Calculate(function, fields)) where the arguments have the following meaning:

function

The name of the calculation function that should be applied to the values.

Possible values are:

SUM

Calculate the sum of the given field values.

AVG

Calculate the average of the given field values.

PRD

Calculate the product of the given field values.

MIN

Calculate the minimum of the given field values.

MAX

Calculate the maximum of the given field values.

fields

An array of AcroForm field names the values of which should be used.



575
576
577
578
579
580
581
582
583
# File 'lib/hexapdf/type/acro_form/java_script_actions.rb', line 575

def run_af_simple_calculate(form, action_string)
  return nil unless (match = AF_SIMPLE_CALCULATE_RE.match(action_string))
  function = match[:function]
  values = match[:fields].scan(/".*?"/).map do |name|
    return nil unless (field = form.field_by_name(name[1..-2]))
    af_make_number(field.field_value)
  end
  AF_SIMPLE_CALCULATE.fetch(function)&.call(values)
end

.run_simplified_field_notation(form, action_string) ⇒ Object

Implements parsing of the simplified field notation (SFN).

The argument form has to be the document’s main AcroForm object and action_string has to be the JavaScript action string.

This notation is more powerful than AFSimple_Calculate as it allows arbitrary expressions consisting of additions, substractions, multiplications and divisions, possibly grouped using parentheses, and field names (which stand in for their value) as well as numbers.

Note: The implementation has been created by looking at sample documents using SFN. As such this may not work for all documents that use SFN.



609
610
611
612
# File 'lib/hexapdf/type/acro_form/java_script_actions.rb', line 609

def run_simplified_field_notation(form, action_string)
  return nil unless (match = /BVCALC(.*?)EVCALC/m.match(action_string))
  SimplifiedFieldNotationParser.new(form, match[1]).parse
end

.simplified_field_notation_action(form, sfn_string) ⇒ Object

Returns the appropriate JavaScript action string for a calculate action that uses Simplified Field Notation.

The argument form has to be the document’s main AcroForm object and sfn_string the string containing the simplified field notation.

See: #run_simplified_field_notation

Raises:

  • (ArgumentError)


592
593
594
595
596
# File 'lib/hexapdf/type/acro_form/java_script_actions.rb', line 592

def simplified_field_notation_action(form, sfn_string)
  js_part = SimplifiedFieldNotationParser.new(form, sfn_string).parse(:generate)
  raise ArgumentError, "Invalid simplified field notation rule" unless js_part
  "/** BVCALC #{sfn_string} EVCALC **/ event.value = #{js_part}"
end