hexapdf 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +33 -0
  3. data/CONTRIBUTERS +1 -1
  4. data/Rakefile +35 -50
  5. data/VERSION +1 -1
  6. data/lib/hexapdf/cli.rb +4 -0
  7. data/lib/hexapdf/cli/command.rb +6 -2
  8. data/lib/hexapdf/cli/image2pdf.rb +141 -0
  9. data/lib/hexapdf/cli/info.rb +1 -1
  10. data/lib/hexapdf/cli/inspect.rb +32 -2
  11. data/lib/hexapdf/cli/modify.rb +1 -1
  12. data/lib/hexapdf/cli/optimize.rb +1 -1
  13. data/lib/hexapdf/cli/watermark.rb +130 -0
  14. data/lib/hexapdf/composer.rb +2 -2
  15. data/lib/hexapdf/configuration.rb +7 -1
  16. data/lib/hexapdf/content/canvas.rb +2 -2
  17. data/lib/hexapdf/content/graphic_object/arc.rb +2 -2
  18. data/lib/hexapdf/content/graphic_object/endpoint_arc.rb +2 -2
  19. data/lib/hexapdf/content/graphic_object/geom2d.rb +1 -1
  20. data/lib/hexapdf/content/graphic_object/solid_arc.rb +1 -1
  21. data/lib/hexapdf/dictionary.rb +11 -3
  22. data/lib/hexapdf/dictionary_fields.rb +32 -3
  23. data/lib/hexapdf/document.rb +7 -3
  24. data/lib/hexapdf/document/files.rb +1 -1
  25. data/lib/hexapdf/document/fonts.rb +21 -1
  26. data/lib/hexapdf/document/pages.rb +2 -2
  27. data/lib/hexapdf/encryption/standard_security_handler.rb +2 -2
  28. data/lib/hexapdf/font/cmap/parser.rb +1 -1
  29. data/lib/hexapdf/font/true_type/table/head.rb +2 -2
  30. data/lib/hexapdf/font/true_type/table/os2.rb +4 -4
  31. data/lib/hexapdf/font/true_type_wrapper.rb +16 -16
  32. data/lib/hexapdf/font/type1_wrapper.rb +16 -16
  33. data/lib/hexapdf/font_loader.rb +2 -0
  34. data/lib/hexapdf/font_loader/from_configuration.rb +5 -0
  35. data/lib/hexapdf/font_loader/standard14.rb +5 -0
  36. data/lib/hexapdf/image_loader/png.rb +1 -1
  37. data/lib/hexapdf/layout/box.rb +2 -2
  38. data/lib/hexapdf/layout/image_box.rb +1 -1
  39. data/lib/hexapdf/layout/style.rb +50 -24
  40. data/lib/hexapdf/layout/text_box.rb +1 -1
  41. data/lib/hexapdf/layout/text_fragment.rb +2 -2
  42. data/lib/hexapdf/layout/text_layouter.rb +14 -10
  43. data/lib/hexapdf/name_tree_node.rb +3 -3
  44. data/lib/hexapdf/number_tree_node.rb +3 -3
  45. data/lib/hexapdf/pdf_array.rb +207 -0
  46. data/lib/hexapdf/rectangle.rb +12 -12
  47. data/lib/hexapdf/serializer.rb +1 -1
  48. data/lib/hexapdf/stream.rb +6 -4
  49. data/lib/hexapdf/task/optimize.rb +3 -3
  50. data/lib/hexapdf/type.rb +2 -0
  51. data/lib/hexapdf/type/acro_form.rb +51 -0
  52. data/lib/hexapdf/type/acro_form/field.rb +129 -0
  53. data/lib/hexapdf/type/acro_form/form.rb +124 -0
  54. data/lib/hexapdf/type/action.rb +1 -1
  55. data/lib/hexapdf/type/actions/go_to.rb +1 -1
  56. data/lib/hexapdf/type/actions/go_to_r.rb +1 -1
  57. data/lib/hexapdf/type/actions/launch.rb +1 -1
  58. data/lib/hexapdf/type/annotation.rb +2 -2
  59. data/lib/hexapdf/type/annotations.rb +1 -0
  60. data/lib/hexapdf/type/annotations/link.rb +4 -15
  61. data/lib/hexapdf/type/annotations/markup_annotation.rb +2 -1
  62. data/lib/hexapdf/type/annotations/text.rb +3 -6
  63. data/lib/hexapdf/type/annotations/widget.rb +90 -0
  64. data/lib/hexapdf/type/catalog.rb +12 -9
  65. data/lib/hexapdf/type/cid_font.rb +3 -3
  66. data/lib/hexapdf/type/file_specification.rb +2 -2
  67. data/lib/hexapdf/type/font_descriptor.rb +5 -2
  68. data/lib/hexapdf/type/font_simple.rb +1 -1
  69. data/lib/hexapdf/type/font_type0.rb +1 -1
  70. data/lib/hexapdf/type/font_type3.rb +1 -1
  71. data/lib/hexapdf/type/form.rb +2 -2
  72. data/lib/hexapdf/type/graphics_state_parameter.rb +11 -6
  73. data/lib/hexapdf/type/icon_fit.rb +58 -0
  74. data/lib/hexapdf/type/image.rb +14 -8
  75. data/lib/hexapdf/type/info.rb +2 -1
  76. data/lib/hexapdf/type/page.rb +4 -4
  77. data/lib/hexapdf/type/page_tree_node.rb +3 -7
  78. data/lib/hexapdf/type/resources.rb +1 -1
  79. data/lib/hexapdf/type/trailer.rb +4 -4
  80. data/lib/hexapdf/type/viewer_preferences.rb +7 -4
  81. data/lib/hexapdf/type/xref_stream.rb +2 -2
  82. data/lib/hexapdf/utils/sorted_tree_node.rb +1 -1
  83. data/lib/hexapdf/version.rb +1 -1
  84. data/man/man1/hexapdf.1 +77 -8
  85. data/test/hexapdf/content/test_canvas.rb +2 -2
  86. data/test/hexapdf/content/test_processor.rb +3 -3
  87. data/test/hexapdf/document/test_files.rb +4 -4
  88. data/test/hexapdf/document/test_fonts.rb +13 -1
  89. data/test/hexapdf/document/test_images.rb +6 -6
  90. data/test/hexapdf/document/test_pages.rb +8 -8
  91. data/test/hexapdf/encryption/test_security_handler.rb +7 -7
  92. data/test/hexapdf/encryption/test_standard_security_handler.rb +5 -5
  93. data/test/hexapdf/font/test_true_type_wrapper.rb +2 -2
  94. data/test/hexapdf/font_loader/test_from_configuration.rb +4 -0
  95. data/test/hexapdf/font_loader/test_standard14.rb +10 -0
  96. data/test/hexapdf/image_loader/test_jpeg.rb +1 -1
  97. data/test/hexapdf/image_loader/test_png.rb +3 -3
  98. data/test/hexapdf/layout/test_box.rb +2 -2
  99. data/test/hexapdf/layout/test_frame.rb +1 -1
  100. data/test/hexapdf/layout/test_image_box.rb +1 -1
  101. data/test/hexapdf/layout/test_style.rb +18 -13
  102. data/test/hexapdf/layout/test_text_box.rb +1 -1
  103. data/test/hexapdf/layout/test_text_layouter.rb +11 -6
  104. data/test/hexapdf/task/test_dereference.rb +2 -2
  105. data/test/hexapdf/task/test_optimize.rb +11 -11
  106. data/test/hexapdf/test_composer.rb +1 -1
  107. data/test/hexapdf/test_dictionary.rb +10 -2
  108. data/test/hexapdf/test_dictionary_fields.rb +27 -3
  109. data/test/hexapdf/test_document.rb +16 -15
  110. data/test/hexapdf/test_importer.rb +4 -4
  111. data/test/hexapdf/test_object.rb +1 -1
  112. data/test/hexapdf/test_pdf_array.rb +162 -0
  113. data/test/hexapdf/test_rectangle.rb +3 -5
  114. data/test/hexapdf/test_serializer.rb +1 -1
  115. data/test/hexapdf/test_stream.rb +1 -0
  116. data/test/hexapdf/test_writer.rb +3 -3
  117. data/test/hexapdf/type/acro_form/test_field.rb +85 -0
  118. data/test/hexapdf/type/acro_form/test_form.rb +69 -0
  119. data/test/hexapdf/type/annotations/test_text.rb +2 -6
  120. data/test/hexapdf/type/annotations/test_widget.rb +24 -0
  121. data/test/hexapdf/type/test_annotation.rb +1 -1
  122. data/test/hexapdf/type/test_catalog.rb +1 -1
  123. data/test/hexapdf/type/test_cid_font.rb +3 -3
  124. data/test/hexapdf/type/test_font.rb +2 -2
  125. data/test/hexapdf/type/test_font_descriptor.rb +2 -1
  126. data/test/hexapdf/type/test_font_simple.rb +3 -3
  127. data/test/hexapdf/type/test_font_true_type.rb +6 -6
  128. data/test/hexapdf/type/test_font_type0.rb +5 -5
  129. data/test/hexapdf/type/test_font_type1.rb +8 -8
  130. data/test/hexapdf/type/test_font_type3.rb +4 -4
  131. data/test/hexapdf/type/test_image.rb +16 -12
  132. data/test/hexapdf/type/test_page.rb +11 -11
  133. data/test/hexapdf/type/test_page_tree_node.rb +20 -20
  134. data/test/hexapdf/type/test_resources.rb +6 -6
  135. data/test/hexapdf/type/test_trailer.rb +5 -2
  136. data/test/hexapdf/type/test_xref_stream.rb +1 -0
  137. data/test/hexapdf/utils/test_sorted_tree_node.rb +35 -35
  138. metadata +23 -7
  139. data/test/hexapdf/type/annotations/test_link.rb +0 -19
@@ -0,0 +1,130 @@
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-2019 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/cli/command'
38
+
39
+ module HexaPDF
40
+ module CLI
41
+
42
+ # Uses one or more pages of one PDF and underlays/overlays it/them onto another.
43
+ class Watermark < Command
44
+
45
+ def initialize #:nodoc:
46
+ super('watermark', takes_commands: false)
47
+ short_desc("Put one or more PDF pages onto another PDF")
48
+ long_desc(<<~EOF)
49
+ This command uses one ore more pages from a PDF file and applies them as background or
50
+ stamp on another PDF file.
51
+
52
+ If multiple pages are selected from the watermark PDF, the --repeat option can be used to
53
+ specify how they should be applied: 'last' (the default) will only repeat the last
54
+ watermark page whereas 'all' will cyclically repeat all watermark pages.
55
+ EOF
56
+
57
+ options.on("-w", "--watermark-file FILE", "The PDF used as watermark") do |watermark_file|
58
+ @watermark_file = watermark_file
59
+ end
60
+ options.on("-i", "--pages PAGES", "The pages of the watermark file that should be used " \
61
+ "(default: 1)") do |pages|
62
+ @pages = pages
63
+ end
64
+ options.on("-r", "--repeat REPEAT_MODE", [:last, :all],
65
+ "Specifies how the watermark pages should be repeated. Either last or " \
66
+ "all (default: last)") do |repeat|
67
+ @repeat = repeat
68
+ end
69
+ options.on("-t", "--type WATERMARK_TYPE", [:background, :stamp],
70
+ "Specifies how the watermark is applied: background applies it below the page " \
71
+ "contents and stamp applies it above. Default: background") do |type|
72
+ @type = (type == :background ? :underlay : :overlay)
73
+ end
74
+ options.on("--password PASSWORD", "-p", String,
75
+ "The password for decrypting the input PDF. Use - for reading from " \
76
+ "standard input.") do |pwd|
77
+ @password = (pwd == '-' ? read_password : pwd)
78
+ end
79
+ define_optimization_options
80
+ define_encryption_options
81
+
82
+ @watermark_file = nil
83
+ @pages = "1"
84
+ @repeat = :last
85
+ @type = :underlay
86
+ @password = nil
87
+ end
88
+
89
+ def execute(in_file, out_file) #:nodoc:
90
+ maybe_raise_on_existing_file(out_file)
91
+ watermark = HexaPDF::Document.open(@watermark_file)
92
+ indices = page_index_generator(watermark)
93
+ xobject_map = {}
94
+ with_document(in_file, password: @password, out_file: out_file) do |doc|
95
+ doc.pages.each do |page|
96
+ index = indices.next
97
+ xobject = xobject_map[index] ||= doc.import(watermark.pages[index].to_form_xobject)
98
+ pw = page.box(:media).width.to_f
99
+ ph = page.box(:media).height.to_f
100
+ xw = xobject.width.to_f
101
+ xh = xobject.height.to_f
102
+ canvas = page.canvas(type: @type)
103
+ ratio = [pw / xw, ph / xh].min
104
+ xw, xh = xw * ratio, xh * ratio
105
+ x, y = (pw - xw) / 2, (ph - xh) / 2
106
+ canvas.xobject(xobject, at: [x, y], width: xw, height: xh)
107
+ end
108
+ end
109
+ end
110
+
111
+ private
112
+
113
+ # Returns an Enumerator instance that returns the indices of the watermark pages that should
114
+ # be used.
115
+ def page_index_generator(watermark)
116
+ pages = parse_pages_specification(@pages, watermark.pages.count)
117
+ Enumerator.new do |y|
118
+ loop do
119
+ pages.each {|index, _rotation| y << index }
120
+ if @repeat == :last
121
+ y << pages.last[0] while true
122
+ end
123
+ end
124
+ end
125
+ end
126
+
127
+ end
128
+
129
+ end
130
+ end
@@ -93,7 +93,7 @@ module HexaPDF
93
93
  # ...
94
94
  # end
95
95
  def self.create(output, **options, &block)
96
- new(options, &block).write(output)
96
+ new(**options, &block).write(output)
97
97
  end
98
98
 
99
99
  # The PDF document that is created.
@@ -295,7 +295,7 @@ module HexaPDF
295
295
  # options to make it work in all cases.
296
296
  def update_style(style, options = {})
297
297
  style ||= base_style
298
- style = style.dup.update(options) unless options.empty?
298
+ style = style.dup.update(**options) unless options.empty?
299
299
  style.font(base_style.font) unless style.font?
300
300
  style.font(@document.fonts.add(style.font)) unless style.font.respond_to?(:dict)
301
301
  style
@@ -199,7 +199,7 @@ module HexaPDF
199
199
  # {"font_name": {variant: file_name, variant2: file_name2, ...}, ...}
200
200
  #
201
201
  # Once a font is registered in this way, the font name together with a variant name can be used
202
- # with the HexaPDF::Document::Fonts#load method to load the font.
202
+ # with the HexaPDF::Document::Fonts#add method to load the font.
203
203
  #
204
204
  # For best compatibility, the following variant names should be used:
205
205
  #
@@ -447,6 +447,10 @@ module HexaPDF
447
447
  Action: 'HexaPDF::Type::Action',
448
448
  XXLaunchActionWinParameters: 'HexaPDF::Type::Actions::Launch::WinParameters',
449
449
  Annot: 'HexaPDF::Type::Annotation',
450
+ XXAppearanceCharacteristics: 'HexaPDF::Type::Annotations::Widget::AppearanceCharacteristics',
451
+ XXIconFit: 'HexaPDF::Type::IconFit',
452
+ XXAcroForm: 'HexaPDF::Type::AcroForm::Form',
453
+ XXAcroFormField: 'HexaPDF::Type::AcroForm::Field',
450
454
  },
451
455
  'object.subtype_map' => {
452
456
  nil => {
@@ -463,6 +467,7 @@ module HexaPDF
463
467
  URI: 'HexaPDF::Type::Actions::URI',
464
468
  Text: 'HexaPDF::Type::Annotations::Text',
465
469
  Link: 'HexaPDF::Type::Annotations::Link',
470
+ Widget: 'HexaPDF::Type::Annotations::Widget',
466
471
  },
467
472
  XObject: {
468
473
  Image: 'HexaPDF::Type::Image',
@@ -485,6 +490,7 @@ module HexaPDF
485
490
  Annot: {
486
491
  Text: 'HexaPDF::Type::Annotations::Text',
487
492
  Link: 'HexaPDF::Type::Annotations::Link',
493
+ Widget: 'HexaPDF::Type::Annotations::Widget',
488
494
  },
489
495
  })
490
496
 
@@ -1088,7 +1088,7 @@ module HexaPDF
1088
1088
  unless obj.respond_to?(:configure)
1089
1089
  obj = context.document.config.constantize('graphic_object.map', obj)
1090
1090
  end
1091
- obj = obj.configure(options) unless options.empty? && obj.respond_to?(:draw)
1091
+ obj = obj.configure(**options) unless options.empty? && obj.respond_to?(:draw)
1092
1092
  obj
1093
1093
  end
1094
1094
 
@@ -1607,7 +1607,7 @@ module HexaPDF
1607
1607
  # See: PDF1.7 s9.2.2
1608
1608
  def font(name = nil, size: nil, **options)
1609
1609
  if name
1610
- @font = (name.respond_to?(:dict) ? name : context.document.fonts.add(name, options))
1610
+ @font = (name.respond_to?(:dict) ? name : context.document.fonts.add(name, **options))
1611
1611
  if size
1612
1612
  font_size(size)
1613
1613
  else
@@ -54,7 +54,7 @@ module HexaPDF
54
54
  #
55
55
  # See #configure for the allowed keyword arguments.
56
56
  def self.configure(**kwargs)
57
- new.configure(kwargs)
57
+ new.configure(**kwargs)
58
58
  end
59
59
 
60
60
  # The maximal number of curves used for approximating a complete ellipse.
@@ -162,7 +162,7 @@ module HexaPDF
162
162
  def draw(canvas, move_to_start: true)
163
163
  @max_curves = canvas.context.document.config['graphic_object.arc.max_curves']
164
164
  canvas.move_to(*start_point) if move_to_start
165
- curves.each {|curve| canvas.curve_to(*curve) }
165
+ curves.each {|x, y, hash| canvas.curve_to(x, y, **hash) }
166
166
  end
167
167
 
168
168
  # Returns an array of arrays that contain the points for the Bezier curves which are used
@@ -54,7 +54,7 @@ module HexaPDF
54
54
  #
55
55
  # See #configure for the allowed keyword arguments.
56
56
  def self.configure(**kwargs)
57
- new.configure(kwargs)
57
+ new.configure(**kwargs)
58
58
  end
59
59
 
60
60
  # x-coordinate of endpoint
@@ -133,7 +133,7 @@ module HexaPDF
133
133
  canvas.line_to(@x, @y)
134
134
  else
135
135
  values = compute_arc_values(x1, y1)
136
- arc = canvas.graphic_object(:arc, values)
136
+ arc = canvas.graphic_object(:arc, **values)
137
137
  arc.draw(canvas, move_to_start: false)
138
138
  end
139
139
  end
@@ -50,7 +50,7 @@ module HexaPDF
50
50
  #
51
51
  # See #configure for the allowed keyword arguments.
52
52
  def self.configure(**kwargs)
53
- new.configure(kwargs)
53
+ new.configure(**kwargs)
54
54
  end
55
55
 
56
56
  # The Geom2D object that should be drawn
@@ -62,7 +62,7 @@ module HexaPDF
62
62
  #
63
63
  # See #configure for the allowed keyword arguments.
64
64
  def self.configure(**kwargs)
65
- new.configure(kwargs)
65
+ new.configure(**kwargs)
66
66
  end
67
67
 
68
68
  # x-coordinate of center point
@@ -70,7 +70,7 @@ module HexaPDF
70
70
  # String:: String (for text strings), PDFByteString (for binary strings)
71
71
  # Date:: PDFDate
72
72
  # Name:: Symbol
73
- # Array:: Array
73
+ # Array:: PDFArray or Array
74
74
  # Dictionary:: Dictionary (or any subclass) or Hash
75
75
  # Stream:: Stream (or any subclass)
76
76
  # Null:: NilClass
@@ -93,11 +93,14 @@ module HexaPDF
93
93
  # to be an indirect object (+true+), a direct object (+false+) or if it doesn't
94
94
  # matter (unspecified or +nil+).
95
95
  #
96
+ # allowed_values:: An array of allowed values for this field.
97
+ #
96
98
  # version:: Specifies the minimum version of the PDF specification needed for this value.
97
99
  def self.define_field(name, type:, required: false, default: nil, indirect: nil,
98
- version: '1.2')
100
+ allowed_values: nil, version: '1.2')
99
101
  @fields ||= {}
100
- @fields[name] = Field.new(type, required, default, indirect, version)
102
+ @fields[name] = Field.new(type, required: required, default: default, indirect: indirect,
103
+ allowed_values: allowed_values, version: version)
101
104
  end
102
105
 
103
106
  # Returns the field entry for the given field name.
@@ -296,6 +299,11 @@ module HexaPDF
296
299
  end
297
300
  end
298
301
 
302
+ # Check the value of the field against the allowed values.
303
+ if field.allowed_values && !field.allowed_values.include?(obj)
304
+ yield("Field #{name} does not contain an allowed value")
305
+ end
306
+
299
307
  # Check if field value needs to be (in)direct
300
308
  unless field.indirect.nil?
301
309
  obj = value[name] # we need the unwrapped object!
@@ -37,6 +37,7 @@
37
37
  require 'time'
38
38
  require 'date'
39
39
  require 'hexapdf/object'
40
+ require 'hexapdf/pdf_array'
40
41
  require 'hexapdf/rectangle'
41
42
  require 'hexapdf/configuration'
42
43
  require 'hexapdf/utils/pdf_doc_encoding'
@@ -101,6 +102,10 @@ module HexaPDF
101
102
  # needs to be a direct object or +nil+ if it can be either.
102
103
  attr_reader :indirect
103
104
 
105
+ # Returns an array with the allowed values for this field, or +nil+ if the values are not
106
+ # constrained.
107
+ attr_reader :allowed_values
108
+
104
109
  # Returns the PDF version that is required for this field.
105
110
  attr_reader :version
106
111
 
@@ -108,10 +113,12 @@ module HexaPDF
108
113
  #
109
114
  # Depending on the +type+ entry an appropriate field converter object is chosen from the
110
115
  # available converters.
111
- def initialize(type, required = false, default = nil, indirect = nil, version = nil)
116
+ def initialize(type, required: false, default: nil, indirect: nil, allowed_values: nil,
117
+ version: nil)
112
118
  @type = [type].flatten
113
119
  @type_mapped = false
114
120
  @required, @default, @indirect, @version = required, default, indirect, version
121
+ @allowed_values = allowed_values && [allowed_values].flatten
115
122
  @converters = @type.map {|t| self.class.converter_for(t) }.compact
116
123
  end
117
124
 
@@ -199,6 +206,27 @@ module HexaPDF
199
206
 
200
207
  end
201
208
 
209
+ # Converter module for fields of type PDFArray.
210
+ module ArrayConverter
211
+
212
+ # This converter is usable if the +type+ is PDFArray.
213
+ def self.usable_for?(type)
214
+ type == PDFArray
215
+ end
216
+
217
+ # PDFArray fields can also contain simple arrays.
218
+ def self.additional_types
219
+ Array
220
+ end
221
+
222
+ # Wraps a given array in the PDFArray class. Otherwise returns +nil+.
223
+ def self.convert(data, _type, document)
224
+ return unless data.kind_of?(Array)
225
+ document.wrap(data, type: PDFArray)
226
+ end
227
+
228
+ end
229
+
202
230
  # Converter module for string fields to automatically convert a string into UTF-8 encoding.
203
231
  module StringConverter
204
232
 
@@ -329,8 +357,9 @@ module HexaPDF
329
357
 
330
358
  end
331
359
 
332
- Field.converters.replace([FileSpecificationConverter, DictionaryConverter, StringConverter,
333
- PDFByteStringConverter, DateConverter, RectangleConverter])
360
+ Field.converters.replace([FileSpecificationConverter, DictionaryConverter, ArrayConverter,
361
+ StringConverter, PDFByteStringConverter, DateConverter,
362
+ RectangleConverter])
334
363
 
335
364
  end
336
365
 
@@ -40,6 +40,7 @@ require 'hexapdf/content'
40
40
  require 'hexapdf/configuration'
41
41
  require 'hexapdf/reference'
42
42
  require 'hexapdf/object'
43
+ require 'hexapdf/pdf_array'
43
44
  require 'hexapdf/stream'
44
45
  require 'hexapdf/revisions'
45
46
  require 'hexapdf/type'
@@ -135,7 +136,7 @@ module HexaPDF
135
136
 
136
137
  @revisions = Revisions.from_io(self, io)
137
138
  @security_handler = if encrypted? && @config['document.auto_decrypt']
138
- Encryption::SecurityHandler.set_up_decryption(self, decryption_opts)
139
+ Encryption::SecurityHandler.set_up_decryption(self, **decryption_opts)
139
140
  else
140
141
  nil
141
142
  end
@@ -199,7 +200,7 @@ module HexaPDF
199
200
  # If the +revision+ option is +:current+, the current revision is used. Otherwise +revision+
200
201
  # should be a revision index.
201
202
  def add(obj, revision: :current, **wrap_opts)
202
- obj = wrap(obj, wrap_opts) unless obj.kind_of?(HexaPDF::Object)
203
+ obj = wrap(obj, **wrap_opts) unless obj.kind_of?(HexaPDF::Object)
203
204
 
204
205
  revision = (revision == :current ? @revisions.current : @revisions.revision(revision))
205
206
  if revision.nil?
@@ -294,7 +295,8 @@ module HexaPDF
294
295
  # prevent invalid mappings when only partial knowledge (:Type key is missing) is available.
295
296
  #
296
297
  # * If there is no valid class after the above steps, HexaPDF::Stream is used if a stream is
297
- # given, HexaPDF::Dictionary if the given objecct is a hash or else HexaPDF::Object is used.
298
+ # given, HexaPDF::Dictionary if the given object is a hash, HexaPDF::PDFArray if it is an
299
+ # array or else HexaPDF::Object is used.
298
300
  #
299
301
  # Options:
300
302
  #
@@ -346,6 +348,8 @@ module HexaPDF
346
348
  HexaPDF::Stream
347
349
  elsif data.value.kind_of?(Hash)
348
350
  HexaPDF::Dictionary
351
+ elsif data.value.kind_of?(Array)
352
+ HexaPDF::PDFArray
349
353
  else
350
354
  HexaPDF::Object
351
355
  end
@@ -83,7 +83,7 @@ module HexaPDF
83
83
  raise ArgumentError, "The name argument is mandatory when given an IO object"
84
84
  end
85
85
 
86
- spec = @document.add(Type: :Filespec)
86
+ spec = @document.add({Type: :Filespec})
87
87
  spec.path = name
88
88
  spec[:Desc] = description if description
89
89
  spec.embed(file_or_io, name: name, register: true) if embed || !file_or_io.kind_of?(String)
@@ -70,11 +70,31 @@ module HexaPDF
70
70
  if font
71
71
  @loaded_fonts_cache[[name, options]] = font
72
72
  else
73
+ font_list = configured_fonts.sort.map do |font_name, variants|
74
+ "#{font_name} (#{variants.join(', ')})"
75
+ end.join(', ')
73
76
  raise HexaPDF::Error, "The requested font '#{name}' in variant '#{options[:variant]}' " \
74
- "couldn't be found"
77
+ "couldn't be found. Configured fonts: #{font_list}"
75
78
  end
76
79
  end
77
80
 
81
+ # Returns a hash of the form 'font_name => [variants, ...]' with all the fonts that are
82
+ # configured. These fonts can be added to the document by using the #add method.
83
+ def configured_fonts
84
+ result = {}
85
+ each_font_loader do |loader|
86
+ next unless loader.respond_to?(:available_fonts)
87
+ loader.available_fonts(@document).each do |name, variants|
88
+ if result.key?(name)
89
+ result[name].concat(variants).uniq!
90
+ else
91
+ result[name] = variants
92
+ end
93
+ end
94
+ end
95
+ result
96
+ end
97
+
78
98
  private
79
99
 
80
100
  # :call-seq: