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
@@ -100,8 +100,10 @@ module HexaPDF
100
100
  # The writing mode of the CMap: 0 for horizontal, 1 for vertical writing.
101
101
  attr_accessor :wmode
102
102
 
103
- attr_reader :codespace_ranges, :cid_mapping, :cid_range_mappings, :unicode_mapping # :nodoc:
104
- protected :codespace_ranges, :cid_mapping, :cid_range_mappings, :unicode_mapping
103
+ attr_reader :codespace_ranges, :cid_mapping, :cid_range_mappings, :unicode_mapping,
104
+ :unicode_range_mappings # :nodoc:
105
+ protected :codespace_ranges, :cid_mapping, :cid_range_mappings, :unicode_mapping,
106
+ :unicode_range_mappings
105
107
 
106
108
  # Creates a new CMap object.
107
109
  def initialize
@@ -109,6 +111,7 @@ module HexaPDF
109
111
  @cid_mapping = {}
110
112
  @cid_range_mappings = []
111
113
  @unicode_mapping = {}
114
+ @unicode_range_mappings = []
112
115
  end
113
116
 
114
117
  # Add all mappings from the given CMap to this CMap.
@@ -117,6 +120,7 @@ module HexaPDF
117
120
  @cid_mapping.merge!(cmap.cid_mapping)
118
121
  @cid_range_mappings.concat(cmap.cid_range_mappings)
119
122
  @unicode_mapping.merge!(cmap.unicode_mapping)
123
+ @unicode_range_mappings.concat(cmap.unicode_range_mappings)
120
124
  end
121
125
 
122
126
  # Add a codespace range using an array of ranges for the individual bytes.
@@ -193,10 +197,25 @@ module HexaPDF
193
197
  @unicode_mapping[code] = string
194
198
  end
195
199
 
200
+ # Adds a mapping from a range of character codes to strings starting with the given 16-bit
201
+ # integer values (representing the raw UTF-16BE characters).
202
+ def add_unicode_range_mapping(start_code, end_code, start_values)
203
+ @unicode_range_mappings << [start_code..end_code, start_values]
204
+ end
205
+
196
206
  # Returns the Unicode string in UTF-8 encoding for the given character code, or +nil+ if no
197
207
  # mapping was found.
198
208
  def to_unicode(code)
199
- unicode_mapping[code]
209
+ @unicode_mapping.fetch(code) do
210
+ @unicode_range_mappings.reverse_each do |range, start_values|
211
+ if range.cover?(code)
212
+ str = start_values[0..-2].append(start_values[-1] + code - range.first).
213
+ pack('n*').encode(::Encoding::UTF_8, ::Encoding::UTF_16BE)
214
+ return @unicode_mapping[code] = str
215
+ end
216
+ end
217
+ nil
218
+ end
200
219
  end
201
220
 
202
221
  end
@@ -62,7 +62,7 @@ module HexaPDF
62
62
  # Specifies whether the font should be subset if possible.
63
63
  #
64
64
  # This method uses the FromFile font loader behind the scenes.
65
- def self.call(document, name, variant: :none, subset: true)
65
+ def self.call(document, name, variant: :none, subset: true, **)
66
66
  file = document.config['font.map'].dig(name, variant)
67
67
  return nil if file.nil?
68
68
 
@@ -0,0 +1,72 @@
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 'hexapdf/font/true_type_wrapper'
38
+ require 'hexapdf/font_loader/from_file'
39
+
40
+ module HexaPDF
41
+ module FontLoader
42
+
43
+ # This module translates font names like 'Helvetica bold' into the arguments 'Helvetica' and
44
+ # {variant: :bold}.
45
+ #
46
+ # This eases the usage of font names where specifying a font variant is not straight-forward.
47
+ # The actual loading of the font is deferred to Document::Fonts#add.
48
+ #
49
+ # Note that this should be the last entry in the list of font loaders to ensure correct
50
+ # operation.
51
+ module VariantFromName
52
+
53
+ # Returns a font wrapper for the given font by splitting the font name into the font name part
54
+ # and variant selector part. If the the resulting font cannot be resolved, +nil+ is returned.
55
+ #
56
+ # A font name should have the form 'Fontname selector' where selector can be 'bold', 'italic'
57
+ # or 'bold_italic', for example 'Helvetica bold'.
58
+ #
59
+ # Note that a supplied :variant keyword argument is ignored!
60
+ def self.call(document, name, recursive_invocation: false, **options)
61
+ return if recursive_invocation
62
+ name, variant = name.split(/ (?=(?:bold|italic|bold_italic)\z)/, 2)
63
+ return if variant.nil?
64
+
65
+ options[:variant] = variant.to_sym
66
+ document.fonts.add(name, **options, recursive_invocation: true) rescue nil
67
+ end
68
+
69
+ end
70
+
71
+ end
72
+ end
@@ -88,6 +88,7 @@ module HexaPDF
88
88
  autoload(:Standard14, 'hexapdf/font_loader/standard14')
89
89
  autoload(:FromConfiguration, 'hexapdf/font_loader/from_configuration')
90
90
  autoload(:FromFile, 'hexapdf/font_loader/from_file')
91
+ autoload(:VariantFromName, 'hexapdf/font_loader/variant_from_name')
91
92
 
92
93
  end
93
94
 
@@ -255,7 +255,9 @@ module HexaPDF
255
255
 
256
256
  @draw_pos_x = frame.x + reserved_width_left
257
257
  @draw_pos_y = frame.y - @height + reserved_height_bottom
258
- @fit_successful = @results.all? {|r| r.box_fitter.fit_successful? } && @results.size == @children.size
258
+ @all_items_fitted = @results.all? {|r| r.box_fitter.fit_successful? } &&
259
+ @results.size == @children.size
260
+ @fit_successful = @all_items_fitted || (@initial_height > 0 && style.overflow == :truncate)
259
261
  end
260
262
 
261
263
  private
@@ -307,7 +309,7 @@ module HexaPDF
307
309
  # Splits the content of the list box. This method is called from Box#split.
308
310
  def split_content(_available_width, _available_height, _frame)
309
311
  remaining_boxes = @results[-1].box_fitter.remaining_boxes
310
- first_is_split_box = remaining_boxes.first&.split_box?
312
+ first_is_split_box = !remaining_boxes.empty?
311
313
  children = (remaining_boxes.empty? ? [] : [remaining_boxes]) + @children[@results.size..-1]
312
314
 
313
315
  box = create_split_box(split_box_value: first_is_split_box ? :hide_first_marker : :show_first_marker)
@@ -323,42 +325,47 @@ module HexaPDF
323
325
  # its contents.
324
326
  def item_marker_box(document, index)
325
327
  return @marker_type.call(document, self, index) if @marker_type.kind_of?(Proc)
326
- return @item_marker_box if defined?(@item_marker_box)
327
-
328
- marker_style = {
329
- font: style.font? ? style.font : document.fonts.add("Times"),
330
- font_size: style.font_size || 10, fill_color: style.fill_color
331
- }
332
- fragment = case @marker_type
333
- when :disc
334
- TextFragment.create("•", marker_style)
335
- when :circle
336
- unless marker_style[:font].decode_codepoint("❍".ord).valid?
337
- marker_style[:font] = document.fonts.add("ZapfDingbats")
338
- end
339
- TextFragment.create("❍", **marker_style,
340
- font_size: style.font_size / 2.0,
341
- text_rise: -style.font_size / 1.8)
342
- when :square
343
- unless marker_style[:font].decode_codepoint("■".ord).valid?
344
- marker_style[:font] = document.fonts.add("ZapfDingbats")
328
+
329
+ unless (fragment = @item_marker_fragment)
330
+ marker_style = {
331
+ font: style.font? ? style.font : document.fonts.add("Times"),
332
+ font_size: style.font_size || 10, fill_color: style.fill_color
333
+ }
334
+ fragment = case @marker_type
335
+ when :disc
336
+ TextFragment.create("•", marker_style)
337
+ when :circle
338
+ unless marker_style[:font].decode_codepoint("❍".ord).valid?
339
+ marker_style[:font] = document.fonts.add("ZapfDingbats")
340
+ end
341
+ TextFragment.create("❍", **marker_style,
342
+ font_size: style.font_size / 2.0,
343
+ text_rise: -style.font_size / 1.8)
344
+ when :square
345
+ unless marker_style[:font].decode_codepoint("■".ord).valid?
346
+ marker_style[:font] = document.fonts.add("ZapfDingbats")
347
+ end
348
+ TextFragment.create("■", **marker_style,
349
+ font_size: style.font_size / 2.0,
350
+ text_rise: -style.font_size / 1.8)
351
+ when :decimal
352
+ text = (@start_number + index).to_s << "."
353
+ TextFragment.create(text, marker_style)
354
+ else
355
+ raise HexaPDF::Error, "Unknown list marker type #{@marker_type.inspect}"
345
356
  end
346
- TextFragment.create("■", **marker_style,
347
- font_size: style.font_size / 2.0,
348
- text_rise: -style.font_size / 1.8)
349
- when :decimal
350
- text = (@start_number + index).to_s << "."
351
- TextFragment.create(text, marker_style)
352
- else
353
- raise HexaPDF::Error, "Unknown list marker type #{@marker_type.inspect}"
354
- end
355
- box = TextBox.new(items: [fragment], style: {text_align: :right, padding: [0, 5, 0, 0]})
356
- @item_marker_box = box unless @marker_type == :decimal
357
- box
357
+ @item_marker_fragment = fragment unless @marker_type == :decimal
358
+ end
359
+ TextBox.new(items: [fragment], style: {text_align: :right, padding: [0, 5, 0, 0]})
358
360
  end
359
361
 
360
362
  # Draws the list items onto the canvas at position [x, y].
361
363
  def draw_content(canvas, x, y)
364
+ if !@all_items_fitted && (@initial_height > 0 && style.overflow == :error)
365
+ raise HexaPDF::Error, "Some items don't fit into box with limited height and " \
366
+ "style property overflow is set to :error"
367
+ end
368
+
362
369
  translate = style.position != :flow && (x != @draw_pos_x || y != @draw_pos_y)
363
370
 
364
371
  if translate
@@ -614,7 +614,7 @@ module HexaPDF
614
614
  # The font to be used, must be set to a valid font wrapper object before it can be used.
615
615
  #
616
616
  # HexaPDF::Composer handles this property specially in that it resolves a set string or array
617
- # to a font wrapper object before doing else with the style object.
617
+ # to a font wrapper object before doing anything else with the style object.
618
618
  #
619
619
  # This is the only style property without a default value!
620
620
  #
@@ -624,11 +624,12 @@ module HexaPDF
624
624
  #
625
625
  # #>pdf-composer100
626
626
  # composer.text("Helvetica", font: composer.document.fonts.add("Helvetica"))
627
- # composer.text("Courier", font: "Courier") # works only with composer
627
+ # composer.text("Courier", font: "Courier")
628
628
  #
629
629
  # helvetica_bold = composer.document.fonts.add("Helvetica", variant: :bold)
630
630
  # composer.text("Helvetica Bold", font: helvetica_bold)
631
- # composer.text("Courier Bold", font: ["Courier", variant: :bold]) # only composer
631
+ # composer.text("Courier Bold", font: "Courier bold")
632
+ # composer.text("Courier Bold also", font: ["Courier", variant: :bold])
632
633
 
633
634
  ##
634
635
  # :method: font_size
@@ -1084,25 +1085,6 @@ module HexaPDF
1084
1085
  # 'Centered',
1085
1086
  # {text: "\u{00a0}", fill_horizontal: 1, overlays: overlays}])
1086
1087
 
1087
- ##
1088
- # :method: text_overflow
1089
- # :call-seq:
1090
- # text_overflow(mode = nil)
1091
- #
1092
- # Specifies how text overflowing a box with a given initial height should be handled:
1093
- #
1094
- # Possible values:
1095
- #
1096
- # :error:: An error is raised (default).
1097
- # :truncate:: Truncates the overflowing text.
1098
- #
1099
- # Examples:
1100
- #
1101
- # #>pdf-composer100
1102
- # composer.text("This is some longer text that does appear in two lines.")
1103
- # composer.text("This is some longer text that does not appear in two lines.",
1104
- # height: 15, text_overflow: :truncate)
1105
-
1106
1088
  ##
1107
1089
  # :method: background_color
1108
1090
  # :call-seq:
@@ -1414,6 +1396,26 @@ module HexaPDF
1414
1396
  # composer.text('Mask covers everything', mask_mode: :fill)
1415
1397
  # composer.text('On the next page')
1416
1398
 
1399
+ ##
1400
+ # :method: overflow
1401
+ # :call-seq:
1402
+ # overflow(mode = nil)
1403
+ #
1404
+ # Specifies how overflowing boxes (e.g. the text of a box or the children of a container) with
1405
+ # a given initial height should be handled:
1406
+ #
1407
+ # Possible values:
1408
+ #
1409
+ # :error:: An error is raised (default).
1410
+ # :truncate:: Truncates the overflowing parts.
1411
+ #
1412
+ # Examples:
1413
+ #
1414
+ # #>pdf-composer100
1415
+ # composer.text("This is some longer text that does appear in two lines.")
1416
+ # composer.text("This is some longer text that does not appear in two lines.",
1417
+ # height: 15, overflow: :truncate)
1418
+
1417
1419
  [
1418
1420
  [:font, "raise HexaPDF::Error, 'No font set'"],
1419
1421
  [:font_size, 10],
@@ -1454,7 +1456,6 @@ module HexaPDF
1454
1456
  extra_args: ", extra_arg = nil"}],
1455
1457
  [:last_line_gap, false, {valid_values: [true, false]}],
1456
1458
  [:fill_horizontal, nil],
1457
- [:text_overflow, :error],
1458
1459
  [:background_color, nil],
1459
1460
  [:background_alpha, 1],
1460
1461
  [:padding, "Quad.new(0)", {setter: "Quad.new(value)"}],
@@ -1467,6 +1468,7 @@ module HexaPDF
1467
1468
  [:valign, :top, {valid_values: [:top, :center, :bottom]}],
1468
1469
  [:mask_mode, :default, {valid_values: [:default, :none, :box, :fill_horizontal,
1469
1470
  :fill_frame_horizontal, :fill_vertical, :fill]}],
1471
+ [:overflow, :error],
1470
1472
  ].each do |name, default, options = {}|
1471
1473
  default = default.inspect unless default.kind_of?(String)
1472
1474
  setter = options.delete(:setter) || "value"
@@ -106,7 +106,7 @@ module HexaPDF
106
106
  end
107
107
 
108
108
  @result.status == :success ||
109
- (@result.status == :height && @initial_height > 0 && style.text_overflow == :truncate)
109
+ (@result.status == :height && @initial_height > 0 && style.overflow == :truncate)
110
110
  end
111
111
 
112
112
  # Splits the text box into two boxes if necessary and possible.
@@ -136,9 +136,9 @@ module HexaPDF
136
136
  def draw_content(canvas, x, y)
137
137
  return unless @result
138
138
 
139
- if @result.status == :height && @initial_height > 0 && style.text_overflow == :error
139
+ if @result.status == :height && @initial_height > 0 && style.overflow == :error
140
140
  raise HexaPDF::Error, "Text doesn't fit into box with limited height and " \
141
- "style property text_overflow is set to :error"
141
+ "style property overflow is set to :error"
142
142
  end
143
143
 
144
144
  return if @result.lines.empty?
@@ -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
@@ -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?
@@ -420,6 +418,26 @@ module HexaPDF
420
418
  not_flattened
421
419
  end
422
420
 
421
+ # Recalculates all form fields that have a calculate action applied (which are all fields
422
+ # listed in the /CO entry).
423
+ #
424
+ # If HexaPDF doesn't support a calculation method or an error occurs during calculation, the
425
+ # field value is not updated.
426
+ #
427
+ # Note that calculations are *not* done automatically when a form field's value changes
428
+ # since it would lead to possibly many calls to this actions. So first fill in all field
429
+ # values and then call this method.
430
+ #
431
+ # See: JavaScriptActions
432
+ def recalculate_fields
433
+ self[:CO]&.each do |field|
434
+ field = Field.wrap(document, field)
435
+ next unless field && (calculation_action = field[:AA]&.[](:C))
436
+ result = JavaScriptActions.calculate(self, calculation_action)
437
+ field.form_field.field_value = result if result
438
+ end
439
+ end
440
+
423
441
  private
424
442
 
425
443
  # Creates a new field with the full name +name+ and the field type +type+.
@@ -468,8 +486,7 @@ module HexaPDF
468
486
  end
469
487
  next false unless field.key?(:T) # Skip widgets
470
488
 
471
- field = document.wrap(field, type: :XXAcroFormField,
472
- subtype: Field.inherited_value(field, :FT))
489
+ field = Field.wrap(document, field)
473
490
  reject = false
474
491
  if field[:Parent] != parent
475
492
  yield("Parent entry of field (#{field.oid},#{field.gen}) invalid", true)