hexapdf 0.12.0 → 0.14.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (99) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +126 -0
  3. data/examples/019-acro_form.rb +41 -4
  4. data/lib/hexapdf/cli/command.rb +4 -2
  5. data/lib/hexapdf/cli/image2pdf.rb +2 -1
  6. data/lib/hexapdf/cli/info.rb +51 -2
  7. data/lib/hexapdf/cli/inspect.rb +30 -8
  8. data/lib/hexapdf/cli/merge.rb +1 -1
  9. data/lib/hexapdf/cli/split.rb +74 -14
  10. data/lib/hexapdf/configuration.rb +15 -0
  11. data/lib/hexapdf/content/graphic_object/arc.rb +3 -3
  12. data/lib/hexapdf/content/parser.rb +1 -1
  13. data/lib/hexapdf/dictionary.rb +4 -4
  14. data/lib/hexapdf/dictionary_fields.rb +1 -9
  15. data/lib/hexapdf/document.rb +41 -16
  16. data/lib/hexapdf/document/files.rb +0 -1
  17. data/lib/hexapdf/encryption/fast_arc4.rb +1 -1
  18. data/lib/hexapdf/encryption/security_handler.rb +1 -0
  19. data/lib/hexapdf/encryption/standard_security_handler.rb +1 -0
  20. data/lib/hexapdf/font/cmap.rb +1 -4
  21. data/lib/hexapdf/font/encoding/base.rb +8 -0
  22. data/lib/hexapdf/font/encoding/difference_encoding.rb +6 -0
  23. data/lib/hexapdf/font/true_type/table/head.rb +1 -0
  24. data/lib/hexapdf/font/true_type/table/os2.rb +2 -0
  25. data/lib/hexapdf/font/type1_wrapper.rb +1 -1
  26. data/lib/hexapdf/image_loader/png.rb +3 -2
  27. data/lib/hexapdf/layout/line.rb +1 -1
  28. data/lib/hexapdf/layout/style.rb +23 -23
  29. data/lib/hexapdf/layout/text_layouter.rb +2 -2
  30. data/lib/hexapdf/layout/text_shaper.rb +3 -2
  31. data/lib/hexapdf/object.rb +52 -25
  32. data/lib/hexapdf/parser.rb +87 -3
  33. data/lib/hexapdf/pdf_array.rb +11 -4
  34. data/lib/hexapdf/revisions.rb +29 -21
  35. data/lib/hexapdf/serializer.rb +1 -1
  36. data/lib/hexapdf/task/optimize.rb +6 -4
  37. data/lib/hexapdf/tokenizer.rb +4 -3
  38. data/lib/hexapdf/type/acro_form/appearance_generator.rb +132 -28
  39. data/lib/hexapdf/type/acro_form/button_field.rb +21 -13
  40. data/lib/hexapdf/type/acro_form/choice_field.rb +68 -14
  41. data/lib/hexapdf/type/acro_form/field.rb +35 -5
  42. data/lib/hexapdf/type/acro_form/form.rb +139 -14
  43. data/lib/hexapdf/type/acro_form/text_field.rb +70 -4
  44. data/lib/hexapdf/type/actions/uri.rb +3 -2
  45. data/lib/hexapdf/type/annotations/widget.rb +3 -4
  46. data/lib/hexapdf/type/catalog.rb +2 -2
  47. data/lib/hexapdf/type/cid_font.rb +1 -1
  48. data/lib/hexapdf/type/file_specification.rb +1 -1
  49. data/lib/hexapdf/type/font.rb +1 -1
  50. data/lib/hexapdf/type/font_simple.rb +4 -2
  51. data/lib/hexapdf/type/font_true_type.rb +6 -2
  52. data/lib/hexapdf/type/font_type0.rb +4 -4
  53. data/lib/hexapdf/type/form.rb +15 -2
  54. data/lib/hexapdf/type/image.rb +2 -2
  55. data/lib/hexapdf/type/page.rb +37 -13
  56. data/lib/hexapdf/type/page_tree_node.rb +29 -5
  57. data/lib/hexapdf/type/resources.rb +1 -0
  58. data/lib/hexapdf/type/trailer.rb +2 -3
  59. data/lib/hexapdf/utils/object_hash.rb +0 -1
  60. data/lib/hexapdf/utils/sorted_tree_node.rb +18 -15
  61. data/lib/hexapdf/version.rb +1 -1
  62. data/test/hexapdf/common_tokenizer_tests.rb +6 -1
  63. data/test/hexapdf/content/graphic_object/test_arc.rb +4 -4
  64. data/test/hexapdf/content/test_canvas.rb +3 -3
  65. data/test/hexapdf/content/test_color_space.rb +1 -1
  66. data/test/hexapdf/encryption/test_aes.rb +4 -4
  67. data/test/hexapdf/encryption/test_standard_security_handler.rb +11 -11
  68. data/test/hexapdf/filter/test_ascii85_decode.rb +1 -1
  69. data/test/hexapdf/filter/test_ascii_hex_decode.rb +1 -1
  70. data/test/hexapdf/font/encoding/test_base.rb +10 -0
  71. data/test/hexapdf/font/encoding/test_difference_encoding.rb +8 -0
  72. data/test/hexapdf/font/test_type1_wrapper.rb +4 -3
  73. data/test/hexapdf/layout/test_style.rb +1 -1
  74. data/test/hexapdf/layout/test_text_layouter.rb +12 -5
  75. data/test/hexapdf/test_configuration.rb +2 -2
  76. data/test/hexapdf/test_dictionary.rb +3 -1
  77. data/test/hexapdf/test_dictionary_fields.rb +2 -2
  78. data/test/hexapdf/test_document.rb +18 -10
  79. data/test/hexapdf/test_object.rb +71 -26
  80. data/test/hexapdf/test_parser.rb +159 -53
  81. data/test/hexapdf/test_pdf_array.rb +8 -1
  82. data/test/hexapdf/test_revisions.rb +35 -0
  83. data/test/hexapdf/test_writer.rb +2 -2
  84. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +296 -38
  85. data/test/hexapdf/type/acro_form/test_button_field.rb +22 -2
  86. data/test/hexapdf/type/acro_form/test_choice_field.rb +92 -9
  87. data/test/hexapdf/type/acro_form/test_field.rb +39 -0
  88. data/test/hexapdf/type/acro_form/test_form.rb +87 -15
  89. data/test/hexapdf/type/acro_form/test_text_field.rb +77 -1
  90. data/test/hexapdf/type/test_font_simple.rb +2 -1
  91. data/test/hexapdf/type/test_font_true_type.rb +6 -0
  92. data/test/hexapdf/type/test_form.rb +26 -1
  93. data/test/hexapdf/type/test_page.rb +45 -7
  94. data/test/hexapdf/type/test_page_tree_node.rb +42 -0
  95. data/test/hexapdf/utils/test_bit_field.rb +2 -0
  96. data/test/hexapdf/utils/test_object_hash.rb +5 -0
  97. data/test/hexapdf/utils/test_sorted_tree_node.rb +10 -9
  98. data/test/test_helper.rb +2 -0
  99. metadata +6 -11
@@ -44,6 +44,9 @@ module HexaPDF
44
44
  # AcroForm text fields provide a box or space to fill-in data entered from keyboard. The text
45
45
  # may be restricted to a single line or can span multiple lines.
46
46
  #
47
+ # A special type of single-line text field is the comb text field. This type of field divides
48
+ # the existing space into /MaxLen equally spaced positions.
49
+ #
47
50
  # == Type Specific Field Flags
48
51
  #
49
52
  # :multiline:: If set, the text field may contain multiple lines.
@@ -88,6 +91,63 @@ module HexaPDF
88
91
  }
89
92
  ).freeze
90
93
 
94
+ # Initializes the text field to be a multiline text field.
95
+ #
96
+ # This method should only be called directly after creating a new text field because it
97
+ # doesn't completely reset the object.
98
+ def initialize_as_multiline_text_field
99
+ flag(:multiline)
100
+ unflag(:file_select, :comb, :password)
101
+ end
102
+
103
+ # Initializes the text field to be a comb text field.
104
+ #
105
+ # This method should only be called directly after creating a new text field because it
106
+ # doesn't completely reset the object.
107
+ def initialize_as_comb_text_field
108
+ flag(:comb)
109
+ unflag(:file_select, :multiline, :password)
110
+ end
111
+
112
+ # Initializes the text field to be a password field.
113
+ #
114
+ # This method should only be called directly after creating a new text field because it
115
+ # doesn't completely reset the object.
116
+ def initialize_as_password_field
117
+ delete(:V)
118
+ flag(:password)
119
+ unflag(:comb, :multiline, :file_select)
120
+ end
121
+
122
+ # Initializes the text field to be a file select field.
123
+ #
124
+ # This method should only be called directly after creating a new text field because it
125
+ # doesn't completely reset the object.
126
+ def initialize_as_file_select_field
127
+ flag(:file_select)
128
+ unflag(:comb, :multiline, :password)
129
+ end
130
+
131
+ # Returns +true+ if this field is a multiline text field.
132
+ def multiline_text_field?
133
+ flagged?(:multiline) && !(flagged?(:file_select) || flagged?(:comb) || flagged?(:password))
134
+ end
135
+
136
+ # Returns +true+ if this field is a comb text field.
137
+ def comb_text_field?
138
+ flagged?(:comb) && !(flagged?(:file_select) || flagged?(:multiline) || flagged?(:password))
139
+ end
140
+
141
+ # Returns +true+ if this field is a password field.
142
+ def password_field?
143
+ flagged?(:password) && !(flagged?(:file_select) || flagged?(:multiline) || flagged?(:comb))
144
+ end
145
+
146
+ # Returns +true+ if this field is a file select field.
147
+ def file_select_field?
148
+ flagged?(:file_select) && !(flagged?(:password) || flagged?(:multiline) || flagged?(:comb))
149
+ end
150
+
91
151
  # Returns the field value, i.e. the text contents of the field, or +nil+ if no value is set.
92
152
  #
93
153
  # Note that modifying the returned value *might not* modify the text contents in case it is
@@ -147,11 +207,16 @@ module HexaPDF
147
207
  #
148
208
  # For information on how this is done see AppearanceGenerator.
149
209
  #
150
- # Note that an appearance for a text field widget is *always* created even if there is an
151
- # existing one to make sure the current field value is properly represented.
152
- def create_appearances
210
+ # Note that no new appearances are created if the field value hasn't changed between
211
+ # invocations.
212
+ #
213
+ # By setting +force+ to +true+ the creation of the appearances can be forced.
214
+ def create_appearances(force: false)
215
+ current_value = field_value
153
216
  appearance_generator_class = document.config.constantize('acro_form.appearance_generator')
154
217
  each_widget do |widget|
218
+ next if !force && widget.cached?(:last_value) && widget.cache(:last_value) == current_value
219
+ widget.cache(:last_value, current_value, update: true)
155
220
  appearance_generator_class.new(widget).create_text_appearances
156
221
  end
157
222
  end
@@ -173,8 +238,9 @@ module HexaPDF
173
238
 
174
239
  if self[:V] && !(self[:V].kind_of?(String) || self[:V].kind_of?(HexaPDF::Stream))
175
240
  yield("Text field doesn't contain text but #{self[:V].class} object")
241
+ return
176
242
  end
177
- if (max_len = self[:MaxLen]) && field_value.length > max_len
243
+ if (max_len = self[:MaxLen]) && field_value && field_value.length > max_len
178
244
  yield("Text contents of field '#{full_field_name}' is too long")
179
245
  end
180
246
  end
@@ -53,9 +53,10 @@ module HexaPDF
53
53
 
54
54
  def perform_validation #:nodoc:
55
55
  super
56
- unless self[:URI].ascii_only?
56
+ uri = self[:URI]
57
+ if uri && !uri.ascii_only?
57
58
  yield("URIs have to contain ASCII characters only", true)
58
- uri = self[:URI].dup.force_encoding(Encoding::BINARY)
59
+ uri = uri.dup.force_encoding(Encoding::BINARY)
59
60
  uri.encode!(Encoding::US_ASCII, fallback: lambda {|c| "%#{c.ord.to_s(16).upcase}" })
60
61
  self[:URI] = uri
61
62
  end
@@ -307,10 +307,9 @@ module HexaPDF
307
307
  color = [0]
308
308
  if (da = self[:DA] || field[:DA])
309
309
  HexaPDF::Content::Parser.parse(da) do |obj, params|
310
- if obj == :rg || obj == :g || obj == :k
311
- color = params.dup
312
- elsif obj == :Tf
313
- size = params[1]
310
+ case obj
311
+ when :rg, :g, :k then color = params.dup
312
+ when :Tf then size = params[1]
314
313
  end
315
314
  end
316
315
  end
@@ -120,12 +120,12 @@ module HexaPDF
120
120
  private
121
121
 
122
122
  # Ensures that there is a valid page tree.
123
- def perform_validation
123
+ def perform_validation(&block)
124
124
  super
125
125
  unless key?(:Pages)
126
126
  yield("A PDF document needs a page tree", true)
127
127
  value[:Pages] = document.add({Type: :Pages})
128
- value[:Pages].validate {|msg, correctable| yield(msg, correctable) }
128
+ value[:Pages].validate(&block)
129
129
  end
130
130
  end
131
131
 
@@ -95,7 +95,7 @@ module HexaPDF
95
95
  #
96
96
  # See: PDF1.7 s9.7.4.3
97
97
  def widths
98
- document.cache(@data, :widths) do
98
+ cache(:widths) do
99
99
  result = {}
100
100
  index = 0
101
101
  array = self[:W] || []
@@ -220,7 +220,7 @@ module HexaPDF
220
220
 
221
221
  if document.catalog.key?(:Names) && document.catalog[:Names].key?(:EmbeddedFiles)
222
222
  tree = document.catalog[:Names][:EmbeddedFiles]
223
- tree.each_entry.find_all {|_, spec| document.deref(spec) == self }.each do |(name, _)|
223
+ tree.each_entry.find_all {|_, spec| spec == self }.each do |(name, _)|
224
224
  tree.delete_entry(name)
225
225
  end
226
226
  end
@@ -102,7 +102,7 @@ module HexaPDF
102
102
 
103
103
  # Parses and caches the ToUnicode CMap.
104
104
  def to_unicode_cmap
105
- document.cache(@data, :to_unicode_cmap) do
105
+ cache(:to_unicode_cmap) do
106
106
  if key?(:ToUnicode)
107
107
  HexaPDF::Font::CMap.parse(self[:ToUnicode].stream)
108
108
  else
@@ -57,7 +57,7 @@ module HexaPDF
57
57
  #
58
58
  # Note that the encoding is cached internally when accessed the first time.
59
59
  def encoding
60
- document.cache(@data, :encoding) do
60
+ cache(:encoding) do
61
61
  case (val = self[:Encoding])
62
62
  when Symbol
63
63
  encoding = HexaPDF::Font::Encoding.for_name(val)
@@ -170,7 +170,9 @@ module HexaPDF
170
170
  [:FirstChar, :LastChar, :Widths].each do |field|
171
171
  yield("Required field #{field} is not set", false) if self[field].nil?
172
172
  end
173
- if self[:Widths].length != (self[:LastChar] - self[:FirstChar] + 1)
173
+
174
+ if key?(:Widths) && key?(:LastChar) && key?(:FirstChar) &&
175
+ self[:Widths].length != (self[:LastChar] - self[:FirstChar] + 1)
174
176
  yield("Invalid number of entries in field Widths", false)
175
177
  end
176
178
  end
@@ -48,8 +48,12 @@ module HexaPDF
48
48
  private
49
49
 
50
50
  def perform_validation
51
- super
52
- yield("Required field FontDescriptor is not set", false) if self[:FontDescriptor].nil?
51
+ std_font = FontType1::StandardFonts.standard_font?(self[:BaseFont])
52
+ super(ignore_missing_font_fields: std_font)
53
+
54
+ if self[:FontDescriptor].nil? && !std_font
55
+ yield("Required field FontDescriptor is not set", false)
56
+ end
53
57
  end
54
58
 
55
59
  end
@@ -58,8 +58,8 @@ module HexaPDF
58
58
 
59
59
  # Returns the CID font of this type 0 font.
60
60
  def descendant_font
61
- document.cache(@data, :descendant_font) do
62
- document.wrap(document.deref(self[:DescendantFonts][0]))
61
+ cache(:descendant_font) do
62
+ document.wrap(self[:DescendantFonts][0])
63
63
  end
64
64
  end
65
65
 
@@ -116,7 +116,7 @@ module HexaPDF
116
116
  #
117
117
  # Note that the CMap is cached internally when accessed the first time.
118
118
  def cmap
119
- document.cache(@data, :cmap) do
119
+ cache(:cmap) do
120
120
  val = self[:Encoding]
121
121
  if val.kind_of?(Symbol)
122
122
  HexaPDF::Font::CMap.for_name(val.to_s)
@@ -135,7 +135,7 @@ module HexaPDF
135
135
  #
136
136
  # See: PDF1.7 s9.10.2
137
137
  def ucs2_cmap
138
- document.cache(@data, :ucs2_cmap) do
138
+ cache(:ucs2_cmap) do
139
139
  encoding = self[:Encoding]
140
140
  system_info = descendant_font[:CIDSystemInfo]
141
141
  registry = system_info[:Registry]
@@ -94,14 +94,18 @@ module HexaPDF
94
94
 
95
95
  # Replaces the contents of the form XObject with the given string.
96
96
  #
97
+ # This also clears the cache to avoid returning invalid objects.
98
+ #
97
99
  # Note: This is the same as #stream= but here for interface compatibility with Page.
98
100
  def contents=(data)
99
101
  self.stream = data
102
+ clear_cache
100
103
  end
101
104
 
102
105
  # Returns the resource dictionary which is automatically created if it doesn't exist.
103
106
  def resources
104
- self[:Resources] ||= document.wrap({}, type: :XXResources)
107
+ self[:Resources] ||= document.wrap({ProcSet: [:PDF, :Text, :ImageB, :ImageC, :ImageI]},
108
+ type: :XXResources)
105
109
  end
106
110
 
107
111
  # Processes the content stream of the form XObject with the given processor object.
@@ -126,13 +130,22 @@ module HexaPDF
126
130
  # The canvas object is cached once it is created so that its graphics state is correctly
127
131
  # retained without the need for parsing its contents.
128
132
  #
133
+ # If the bounding box of the form XObject doesn't have its origin at (0, 0), the canvas origin
134
+ # is translated into the bottom left corner so that this detail doesn't matter when using the
135
+ # canvas. This means that the canvas' origin is always at the bottom left corner of the
136
+ # bounding box.
137
+ #
129
138
  # *Note* that a canvas can only be retrieved for initially empty form XObjects!
130
139
  def canvas
131
- document.cache(@data, :canvas) do
140
+ cache(:canvas) do
132
141
  unless stream.empty?
133
142
  raise HexaPDF::Error, "Cannot create a canvas for a form XObjects with contents"
134
143
  end
144
+
135
145
  canvas = Content::Canvas.new(self)
146
+ if box.left != 0 || box.bottom != 0
147
+ canvas.save_graphics_state.translate(box.left, box.bottom)
148
+ end
136
149
  self.stream = canvas.stream_data
137
150
  set_filter(:FlateDecode)
138
151
  canvas
@@ -150,7 +150,7 @@ module HexaPDF
150
150
  color_space, = *self[:ColorSpace]
151
151
  if color_space == :Indexed
152
152
  result.indexed = true
153
- color_space, = *document.deref(self[:ColorSpace][1])
153
+ color_space, = *self[:ColorSpace][1]
154
154
  end
155
155
  case color_space
156
156
  when :DeviceRGB, :CalRGB
@@ -240,7 +240,7 @@ module HexaPDF
240
240
  end
241
241
 
242
242
  if color_type == ImageLoader::PNG::INDEXED
243
- palette_data = document.deref(self[:ColorSpace][3])
243
+ palette_data = self[:ColorSpace][3]
244
244
  palette_data = palette_data.stream unless palette_data.kind_of?(String)
245
245
  palette = ''.b
246
246
  if info.color_space == :rgb
@@ -304,7 +304,7 @@ module HexaPDF
304
304
  def contents
305
305
  Array(self[:Contents]).each_with_object("".b) do |content_stream, content|
306
306
  content << " " unless content.empty?
307
- content << document.deref(content_stream).stream
307
+ content << content_stream.stream
308
308
  end
309
309
  end
310
310
 
@@ -323,10 +323,11 @@ module HexaPDF
323
323
  end
324
324
  end
325
325
 
326
- # Returns the possibly inherited resource dictionary which is automatically created if it
326
+ # Returns the, possibly inherited, resource dictionary which is automatically created if it
327
327
  # doesn't exist.
328
328
  def resources
329
- self[:Resources] ||= document.wrap({}, type: :XXResources)
329
+ self[:Resources] ||= document.wrap({ProcSet: [:PDF, :Text, :ImageB, :ImageC, :ImageI]},
330
+ type: :XXResources)
330
331
  end
331
332
 
332
333
  # Processes the content streams associated with the page with the given processor object.
@@ -344,8 +345,7 @@ module HexaPDF
344
345
  node = self
345
346
  while (parent_node = node[:Parent])
346
347
  parent_node[:Kids].each do |kid|
347
- kid = document.deref(kid)
348
- break if kid.data == node.data
348
+ break if kid == node
349
349
  idx += (kid.type == :Page ? 1 : kid[:Count])
350
350
  end
351
351
  node = parent_node
@@ -353,11 +353,26 @@ module HexaPDF
353
353
  idx
354
354
  end
355
355
 
356
+ # Returns all parent nodes of the page up to the root of the page tree.
357
+ #
358
+ # The direct parent is the first node in the array and the root node the last.
359
+ def ancestor_nodes
360
+ parent = self[:Parent]
361
+ result = [parent]
362
+ result << parent while (parent = parent[:Parent])
363
+ result
364
+ end
365
+
356
366
  # Returns the requested type of canvas for the page.
357
367
  #
358
368
  # The canvas object is cached once it is created so that its graphics state is correctly
359
369
  # retained without the need for parsing its contents.
360
370
  #
371
+ # If the media box of the page doesn't have its origin at (0, 0), the canvas origin is
372
+ # translated into the bottom left corner so that this detail doesn't matter when using the
373
+ # canvas. This means that the canvas' origin is always at the bottom left corner of the media
374
+ # box.
375
+ #
361
376
  # type::
362
377
  # Can either be
363
378
  # * :page for getting the canvas for the page itself (only valid for initially empty pages)
@@ -368,22 +383,31 @@ module HexaPDF
368
383
  raise ArgumentError, "Invalid value for 'type', expected: :page, :underlay or :overlay"
369
384
  end
370
385
  cache_key = "#{type}_canvas".intern
371
- return document.cache(@data, cache_key) if document.cached?(@data, cache_key)
386
+ return cache(cache_key) if cached?(cache_key)
372
387
 
373
388
  if type == :page && key?(:Contents)
374
389
  raise HexaPDF::Error, "Cannot get the canvas for a page with contents"
375
390
  end
376
391
 
392
+ create_canvas = lambda do
393
+ Content::Canvas.new(self).tap do |canvas|
394
+ media_box = box(:media)
395
+ if media_box.left != 0 || media_box.bottom != 0
396
+ canvas.translate(media_box.left, media_box.bottom)
397
+ end
398
+ end
399
+ end
400
+
377
401
  contents = self[:Contents]
378
402
  if contents.nil?
379
- page_canvas = document.cache(@data, :page_canvas, Content::Canvas.new(self))
403
+ page_canvas = cache(:page_canvas, create_canvas.call)
380
404
  self[:Contents] = document.add({Filter: :FlateDecode},
381
405
  stream: page_canvas.stream_data)
382
406
  end
383
407
 
384
408
  if type == :overlay || type == :underlay
385
- underlay_canvas = document.cache(@data, :underlay_canvas, Content::Canvas.new(self))
386
- overlay_canvas = document.cache(@data, :overlay_canvas, Content::Canvas.new(self))
409
+ underlay_canvas = cache(:underlay_canvas, create_canvas.call)
410
+ overlay_canvas = cache(:overlay_canvas, create_canvas.call)
387
411
 
388
412
  stream = HexaPDF::StreamData.new do
389
413
  Fiber.yield(" q ")
@@ -396,18 +420,19 @@ module HexaPDF
396
420
  underlay = document.add({Filter: :FlateDecode}, stream: stream)
397
421
 
398
422
  stream = HexaPDF::StreamData.new do
399
- Fiber.yield(" Q ")
423
+ Fiber.yield(" Q q ")
400
424
  fiber = overlay_canvas.stream_data.fiber
401
425
  while fiber.alive? && (data = fiber.resume)
402
426
  Fiber.yield(data)
403
427
  end
428
+ " Q "
404
429
  end
405
430
  overlay = document.add({Filter: :FlateDecode}, stream: stream)
406
431
 
407
432
  self[:Contents] = [underlay, *self[:Contents], overlay]
408
433
  end
409
434
 
410
- document.cache(@data, cache_key)
435
+ cache(cache_key)
411
436
  end
412
437
 
413
438
  # Creates a Form XObject from the page's dictionary and contents for the given PDF document.
@@ -448,8 +473,7 @@ module HexaPDF
448
473
  REQUIRED_INHERITABLE_FIELDS.each do |name|
449
474
  next if self[name]
450
475
  yield("Inheritable page field #{name} not set", name == :Resources)
451
- self[:Resources] = {}
452
- self[:Resources].validate(&block)
476
+ resources.validate(&block) if name == :Ressources
453
477
  end
454
478
  end
455
479
 
@@ -172,11 +172,10 @@ module HexaPDF
172
172
  return unless page && !page.null? && page[:Parent]
173
173
 
174
174
  parent = page[:Parent]
175
- index = parent[:Kids].index {|kid| kid.data == page.data }
175
+ index = parent[:Kids].index(page)
176
176
 
177
177
  if index
178
- ancestors = [parent]
179
- ancestors << parent while (parent = parent[:Parent])
178
+ ancestors = page.ancestor_nodes
180
179
  return nil unless ancestors.include?(self)
181
180
 
182
181
  page[:Parent][:Kids].delete_at(index)
@@ -188,6 +187,31 @@ module HexaPDF
188
187
  end
189
188
  end
190
189
 
190
+ # :call-seq:
191
+ # pages.move_page(page, to_index)
192
+ # pages.move_page(index, to_index)
193
+ #
194
+ # Moves the given page or the page at the position specified by the zero-based index to the
195
+ # +to_index+ position.
196
+ #
197
+ # If the page that should be moved, doesn't exist or is invalid, an error is raised.
198
+ #
199
+ # Negative indices count backwards from the end, i.e. -1 is the last page. When using a
200
+ # negative index, the page will be moved after that element. So using an index of -1 will
201
+ # move the page after the last page.
202
+ def move_page(page, to_index)
203
+ page = self.page(page) if page.kind_of?(Integer)
204
+ if page.nil? || page.null? || !page[:Parent] ||
205
+ !(ancestors = page.ancestor_nodes).include?(self)
206
+ raise HexaPDF::Error, "The page to be moved doesn't exist in this page tree"
207
+ end
208
+
209
+ parent = page[:Parent]
210
+ insert_page(to_index, page)
211
+ ancestors.each {|node| node[:Count] -= 1 }
212
+ parent[:Kids].delete(page)
213
+ end
214
+
191
215
  # :call-seq:
192
216
  # pages.each_page {|page| block } -> pages
193
217
  # pages.each_page -> Enumerator
@@ -222,7 +246,7 @@ module HexaPDF
222
246
  # Ensures that the /Count and /Parent fields of the whole page tree are set up correctly and
223
247
  # that there is at least one page node. This is therefore only done for the root node of the
224
248
  # page tree!
225
- def perform_validation
249
+ def perform_validation(&block)
226
250
  super
227
251
  return if key?(:Parent)
228
252
 
@@ -255,7 +279,7 @@ module HexaPDF
255
279
 
256
280
  if self[:Count] == 0
257
281
  yield("A PDF document needs at least one page", true)
258
- add_page.validate {|msg, correctable| yield(msg, correctable) }
282
+ add_page.validate(&block)
259
283
  end
260
284
  end
261
285