hexapdf 0.40.0 → 0.41.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -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
 
@@ -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
@@ -1413,7 +1414,7 @@ module HexaPDF
1413
1414
  # #>pdf-composer100
1414
1415
  # composer.text("This is some longer text that does appear in two lines.")
1415
1416
  # composer.text("This is some longer text that does not appear in two lines.",
1416
- # height: 15, text_overflow: :truncate)
1417
+ # height: 15, overflow: :truncate)
1417
1418
 
1418
1419
  [
1419
1420
  [:font, "raise HexaPDF::Error, 'No font set'"],
@@ -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)