hexapdf 0.26.2 → 0.28.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 (83) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +115 -1
  3. data/README.md +1 -1
  4. data/examples/013-text_layouter_shapes.rb +8 -8
  5. data/examples/016-frame_automatic_box_placement.rb +3 -3
  6. data/examples/017-frame_text_flow.rb +3 -3
  7. data/examples/019-acro_form.rb +14 -3
  8. data/examples/020-column_box.rb +3 -3
  9. data/examples/023-images.rb +30 -0
  10. data/lib/hexapdf/cli/info.rb +5 -1
  11. data/lib/hexapdf/cli/inspect.rb +2 -2
  12. data/lib/hexapdf/cli/split.rb +8 -8
  13. data/lib/hexapdf/cli/watermark.rb +2 -2
  14. data/lib/hexapdf/configuration.rb +3 -2
  15. data/lib/hexapdf/content/canvas.rb +8 -3
  16. data/lib/hexapdf/dictionary.rb +4 -17
  17. data/lib/hexapdf/document/destinations.rb +42 -5
  18. data/lib/hexapdf/document/signatures.rb +265 -48
  19. data/lib/hexapdf/document.rb +6 -10
  20. data/lib/hexapdf/filter/ascii85_decode.rb +1 -1
  21. data/lib/hexapdf/importer.rb +35 -27
  22. data/lib/hexapdf/layout/list_box.rb +1 -5
  23. data/lib/hexapdf/object.rb +5 -0
  24. data/lib/hexapdf/parser.rb +14 -0
  25. data/lib/hexapdf/revision.rb +15 -12
  26. data/lib/hexapdf/revisions.rb +7 -1
  27. data/lib/hexapdf/tokenizer.rb +15 -9
  28. data/lib/hexapdf/type/acro_form/appearance_generator.rb +174 -128
  29. data/lib/hexapdf/type/acro_form/button_field.rb +5 -3
  30. data/lib/hexapdf/type/acro_form/choice_field.rb +2 -0
  31. data/lib/hexapdf/type/acro_form/field.rb +11 -5
  32. data/lib/hexapdf/type/acro_form/form.rb +61 -8
  33. data/lib/hexapdf/type/acro_form/signature_field.rb +2 -0
  34. data/lib/hexapdf/type/acro_form/text_field.rb +12 -2
  35. data/lib/hexapdf/type/annotations/widget.rb +3 -0
  36. data/lib/hexapdf/type/catalog.rb +1 -1
  37. data/lib/hexapdf/type/font_true_type.rb +14 -0
  38. data/lib/hexapdf/type/object_stream.rb +2 -2
  39. data/lib/hexapdf/type/outline.rb +19 -1
  40. data/lib/hexapdf/type/outline_item.rb +72 -14
  41. data/lib/hexapdf/type/page.rb +95 -64
  42. data/lib/hexapdf/type/resources.rb +13 -17
  43. data/lib/hexapdf/type/signature/adbe_pkcs7_detached.rb +16 -2
  44. data/lib/hexapdf/type/signature.rb +10 -0
  45. data/lib/hexapdf/version.rb +1 -1
  46. data/lib/hexapdf/writer.rb +5 -3
  47. data/test/hexapdf/content/test_canvas.rb +5 -0
  48. data/test/hexapdf/document/test_destinations.rb +41 -0
  49. data/test/hexapdf/document/test_pages.rb +2 -2
  50. data/test/hexapdf/document/test_signatures.rb +139 -19
  51. data/test/hexapdf/encryption/test_aes.rb +1 -1
  52. data/test/hexapdf/filter/test_predictor.rb +0 -1
  53. data/test/hexapdf/layout/test_box.rb +2 -1
  54. data/test/hexapdf/layout/test_column_box.rb +1 -1
  55. data/test/hexapdf/layout/test_list_box.rb +1 -1
  56. data/test/hexapdf/test_document.rb +2 -8
  57. data/test/hexapdf/test_importer.rb +27 -6
  58. data/test/hexapdf/test_parser.rb +19 -2
  59. data/test/hexapdf/test_revision.rb +15 -14
  60. data/test/hexapdf/test_revisions.rb +63 -12
  61. data/test/hexapdf/test_stream.rb +1 -1
  62. data/test/hexapdf/test_tokenizer.rb +10 -1
  63. data/test/hexapdf/test_writer.rb +11 -3
  64. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +135 -56
  65. data/test/hexapdf/type/acro_form/test_button_field.rb +6 -1
  66. data/test/hexapdf/type/acro_form/test_choice_field.rb +4 -0
  67. data/test/hexapdf/type/acro_form/test_field.rb +4 -4
  68. data/test/hexapdf/type/acro_form/test_form.rb +65 -0
  69. data/test/hexapdf/type/acro_form/test_signature_field.rb +4 -0
  70. data/test/hexapdf/type/acro_form/test_text_field.rb +13 -0
  71. data/test/hexapdf/type/signature/common.rb +54 -0
  72. data/test/hexapdf/type/signature/test_adbe_pkcs7_detached.rb +21 -0
  73. data/test/hexapdf/type/test_catalog.rb +5 -2
  74. data/test/hexapdf/type/test_font_true_type.rb +20 -0
  75. data/test/hexapdf/type/test_object_stream.rb +2 -1
  76. data/test/hexapdf/type/test_outline.rb +4 -1
  77. data/test/hexapdf/type/test_outline_item.rb +62 -1
  78. data/test/hexapdf/type/test_page.rb +103 -45
  79. data/test/hexapdf/type/test_page_tree_node.rb +4 -2
  80. data/test/hexapdf/type/test_resources.rb +0 -5
  81. data/test/hexapdf/type/test_signature.rb +8 -0
  82. data/test/test_helper.rb +1 -1
  83. metadata +61 -4
@@ -84,6 +84,8 @@ module HexaPDF
84
84
  # See: PDF1.7 s12.7.4.2
85
85
  class ButtonField < Field
86
86
 
87
+ define_type :XXAcroFormField
88
+
87
89
  define_field :Opt, type: PDFArray, version: '1.4'
88
90
 
89
91
  # All inheritable dictionary fields for button fields.
@@ -206,7 +208,7 @@ module HexaPDF
206
208
  # Note that this will only return useful values if there is at least one correctly set-up
207
209
  # widget.
208
210
  def allowed_values
209
- (each_widget.each_with_object([]) do |widget, result|
211
+ (each_widget.with_object([]) do |widget, result|
210
212
  keys = widget.appearance_dict&.normal_appearance&.value&.keys
211
213
  result.concat(keys) if keys
212
214
  end - [:Off]).uniq
@@ -254,14 +256,14 @@ module HexaPDF
254
256
  normal_appearance = widget.appearance_dict&.normal_appearance
255
257
  next if !force && normal_appearance &&
256
258
  ((!push_button? && normal_appearance.value.length == 2 &&
257
- normal_appearance.value.each_value.all?(HexaPDF::Stream)) ||
259
+ normal_appearance.each.all? {|_, v| v.kind_of?(HexaPDF::Stream) }) ||
258
260
  (push_button? && normal_appearance.kind_of?(HexaPDF::Stream)))
259
261
  if check_box?
260
262
  appearance_generator_class.new(widget).create_check_box_appearances
261
263
  elsif radio_button?
262
264
  appearance_generator_class.new(widget).create_radio_button_appearances
263
265
  else
264
- raise HexaPDF::Error, "Push buttons not yet supported"
266
+ appearance_generator_class.new(widget).create_push_button_appearances
265
267
  end
266
268
  end
267
269
  end
@@ -69,6 +69,8 @@ module HexaPDF
69
69
  # See: PDF1.7 s12.7.4.4
70
70
  class ChoiceField < VariableTextField
71
71
 
72
+ define_type :XXAcroFormField
73
+
72
74
  define_field :Opt, type: PDFArray
73
75
  define_field :TI, type: Integer, default: 0
74
76
  define_field :I, type: PDFArray, version: '1.4'
@@ -242,8 +242,8 @@ module HexaPDF
242
242
  end
243
243
 
244
244
  # :call-seq:
245
- # field.each_widget {|widget| block} -> field
246
- # field.each_widget -> Enumerator
245
+ # field.each_widget(direct_only: true) {|widget| block} -> field
246
+ # field.each_widget(direct_only: true) -> Enumerator
247
247
  #
248
248
  # Yields each widget, i.e. visual representation, of this field.
249
249
  #
@@ -253,11 +253,17 @@ module HexaPDF
253
253
  # 2. One or more widgets are defined as children of this field.
254
254
  # 3. Widgets of *another field instance with the same full field name*.
255
255
  #
256
- # Because of possibility 3 all fields of the form have to be searched to check whether there
257
- # is another field with the same full field name.
256
+ # With the default of +direct_only+ being +true+, only the usual cases 1 and 2 are handled/
257
+ # If case 3 also needs to be handled, set +direct_only+ to +false+ or run the validation on
258
+ # the main AcroForm object (HexaPDF::Document#acro_form) before using this method (this will
259
+ # reduce case 3 to case 2).
260
+ #
261
+ # *Note*: Setting +direct_only+ to +false+ will have a severe performance impact since all
262
+ # fields of the form have to be searched to check whether there is another field with the
263
+ # same full field name.
258
264
  #
259
265
  # See: HexaPDF::Type::Annotations::Widget
260
- def each_widget(direct_only: false, &block) # :yields: widget
266
+ def each_widget(direct_only: true, &block) # :yields: widget
261
267
  return to_enum(__method__, direct_only: direct_only) unless block_given?
262
268
 
263
269
  if embedded_widget?
@@ -125,14 +125,22 @@ module HexaPDF
125
125
  def each_field(terminal_only: true)
126
126
  return to_enum(__method__, terminal_only: terminal_only) unless block_given?
127
127
 
128
- process_field = lambda do |field|
129
- field = document.wrap(field, type: :XXAcroFormField,
130
- subtype: Field.inherited_value(field, :FT))
131
- yield(field) if field.terminal_field? || !terminal_only
132
- field[:Kids].each(&process_field) unless field.terminal_field?
128
+ process_field_array = lambda do |array|
129
+ array.each_with_index do |field, index|
130
+ unless field.respond_to?(:type) && field.type == :XXAcroFormField
131
+ array[index] = field = document.wrap(field, type: :XXAcroFormField,
132
+ subtype: Field.inherited_value(field, :FT))
133
+ end
134
+ if field.terminal_field?
135
+ yield(field)
136
+ else
137
+ yield(field) unless terminal_only
138
+ process_field_array.call(field[:Kids])
139
+ end
140
+ end
133
141
  end
134
142
 
135
- root_fields.each(&process_field)
143
+ process_field_array.call(root_fields)
136
144
  self
137
145
  end
138
146
 
@@ -393,9 +401,10 @@ module HexaPDF
393
401
  fields.each {|field| field.create_appearances if field.respond_to?(:create_appearances) }
394
402
  end
395
403
 
396
- not_flattened = fields.map {|field| field.each_widget.to_a }.flatten
404
+ not_flattened = fields.map {|field| field.each_widget(direct_only: true).to_a }.flatten
397
405
  document.pages.each {|page| not_flattened = page.flatten_annotations(not_flattened) }
398
- fields -= not_flattened.map(&:form_field)
406
+ not_flattened.map!(&:form_field)
407
+ fields -= not_flattened
399
408
 
400
409
  fields.each do |field|
401
410
  (field[:Parent]&.[](:Kids) || self[:Fields]).delete(field)
@@ -448,6 +457,50 @@ module HexaPDF
448
457
  def perform_validation # :nodoc:
449
458
  super
450
459
 
460
+ seen = {} # used for combining field
461
+
462
+ validate_array = lambda do |parent, container|
463
+ container.reject! do |field|
464
+ if !field.kind_of?(HexaPDF::Object) || !field.kind_of?(HexaPDF::Dictionary) || field.null?
465
+ yield("Invalid object in AcroForm field hierarchy", true)
466
+ next true
467
+ end
468
+ next false unless field.key?(:T) # Skip widgets
469
+
470
+ field = document.wrap(field, type: :XXAcroFormField,
471
+ subtype: Field.inherited_value(field, :FT))
472
+ reject = false
473
+ if field[:Parent] != parent
474
+ yield("Parent entry of field (#{field.oid},#{field.gen}) invalid", true)
475
+ if field[:Parent].nil?
476
+ root_fields << field
477
+ reject = true
478
+ else
479
+ field[:Parent] = parent
480
+ end
481
+ end
482
+
483
+ # Combine fields with same name
484
+ name = field.full_field_name
485
+ if (other_field = seen[name])
486
+ kids = other_field[:Kids] ||= []
487
+ kids << other_field.send(:extract_widget) if other_field.embedded_widget?
488
+ widgets = field.embedded_widget? ? [field.send(:extract_widget)] : field.each_widget.to_a
489
+ widgets.each do |widget|
490
+ widget[:Parent] = other_field
491
+ kids << widget
492
+ end
493
+ reject = true
494
+ elsif !reject
495
+ seen[name] = field
496
+ end
497
+
498
+ validate_array.call(field, field[:Kids]) if field.key?(:Kids)
499
+ reject
500
+ end
501
+ end
502
+ validate_array.call(nil, root_fields)
503
+
451
504
  if (da = self[:DA])
452
505
  unless self[:DR]
453
506
  yield("When the field /DA is present, the field /DR must also be present")
@@ -192,6 +192,8 @@ module HexaPDF
192
192
 
193
193
  end
194
194
 
195
+ define_type :XXAcroFormField
196
+
195
197
  define_field :Lock, type: :SigFieldLock, indirect: true, version: '1.5'
196
198
  define_field :SV, type: :SV, indirect: true, version: '1.5'
197
199
 
@@ -73,6 +73,8 @@ module HexaPDF
73
73
  # See: PDF1.7 s12.7.4.3
74
74
  class TextField < VariableTextField
75
75
 
76
+ define_type :XXAcroFormField
77
+
76
78
  define_field :MaxLen, type: Integer
77
79
 
78
80
  # All inheritable dictionary fields for text fields.
@@ -220,7 +222,15 @@ module HexaPDF
220
222
  current_value = field_value
221
223
  appearance_generator_class = document.config.constantize('acro_form.appearance_generator')
222
224
  each_widget do |widget|
223
- next if !force && widget.cached?(:last_value) && widget.cache(:last_value) == current_value
225
+ is_cached = widget.cached?(:last_value)
226
+ unless force
227
+ if is_cached && widget.cache(:last_value) == current_value
228
+ next
229
+ elsif !is_cached && widget.appearance?
230
+ widget.cache(:last_value, current_value, update: true)
231
+ next
232
+ end
233
+ end
224
234
  widget.cache(:last_value, current_value, update: true)
225
235
  appearance_generator_class.new(widget).create_text_appearances
226
236
  end
@@ -228,7 +238,7 @@ module HexaPDF
228
238
 
229
239
  # Updates the widgets so that they reflect the current field value.
230
240
  def update_widgets
231
- create_appearances
241
+ create_appearances(force: true)
232
242
  end
233
243
 
234
244
  private
@@ -224,6 +224,9 @@ module HexaPDF
224
224
  # The kind of marker that is shown inside the widget. Can either be one of the symbols
225
225
  # +:check+, +:circle+, +:cross+, +:diamond+, +:square+ or +:star+, or a one character
226
226
  # string. The latter is interpreted using the ZapfDingbats font.
227
+ #
228
+ # If an empty string is set, it is treated as if +nil+ was set, i.e. it shows the default
229
+ # marker for the field type.
227
230
  attr_reader :style
228
231
 
229
232
  # The size of the marker in PDF points that is shown inside the widget. The special value
@@ -65,7 +65,7 @@ module HexaPDF
65
65
  :TwoPageLeft, :TwoPageRight]
66
66
  define_field :PageMode, type: Symbol, default: :UseNone,
67
67
  allowed_values: [:UseNone, :UseOutlines, :UseThumbs, :FullScreen, :UseOC, :UseAttachments]
68
- define_field :Outlines, type: Dictionary, indirect: true
68
+ define_field :Outlines, type: :Outlines, indirect: true
69
69
  define_field :Threads, type: PDFArray, version: '1.1'
70
70
  define_field :OpenAction, type: [Dictionary, PDFArray], version: '1.1'
71
71
  define_field :AA, type: Dictionary, version: '1.4'
@@ -35,6 +35,7 @@
35
35
  #++
36
36
 
37
37
  require 'hexapdf/type/font_simple'
38
+ require 'hexapdf/font/true_type_wrapper'
38
39
 
39
40
  module HexaPDF
40
41
  module Type
@@ -45,6 +46,19 @@ module HexaPDF
45
46
  define_field :Subtype, type: Symbol, required: true, default: :TrueType
46
47
  define_field :BaseFont, type: Symbol, required: true
47
48
 
49
+ # Overrides the default to provide a font wrapper in case none is set and a complete TrueType
50
+ # is embedded.
51
+ #
52
+ # See: Font#font_wrapper
53
+ def font_wrapper
54
+ if (tmp = super)
55
+ tmp
56
+ elsif (font_file = self.font_file) && self[:BaseFont].to_s !~ /\A[A-Z]{6}\+/
57
+ font = HexaPDF::Font::TrueType::Font.new(StringIO.new(font_file.stream))
58
+ @font_wrapper = HexaPDF::Font::TrueTypeWrapper.new(document, font, subset: true)
59
+ end
60
+ end
61
+
48
62
  private
49
63
 
50
64
  def perform_validation
@@ -179,7 +179,7 @@ module HexaPDF
179
179
  # Due to a bug in Adobe Acrobat, the Catalog may not be in an object stream if the
180
180
  # document is encrypted
181
181
  if obj.nil? || obj.null? || obj.gen != 0 || obj.kind_of?(Stream) || obj == encrypt_dict ||
182
- (encrypt_dict && obj.type == :Catalog) ||
182
+ obj.type == :Catalog ||
183
183
  obj.type == :Sig || obj.type == :DocTimeStamp ||
184
184
  (obj.respond_to?(:key?) && obj.key?(:ByteRange) && obj.key?(:Contents))
185
185
  delete_object(objects[index])
@@ -220,7 +220,7 @@ module HexaPDF
220
220
 
221
221
  # Returns the container with the to-be-stored objects.
222
222
  def objects
223
- @objects ||=
223
+ @objects ||=
224
224
  begin
225
225
  @objects = {}
226
226
  parse_stream
@@ -50,6 +50,8 @@ module HexaPDF
50
50
  # The outline dictionary is linked via the /Outlines entry from the Type::Catalog and can
51
51
  # directly be accessed via HexaPDF::Document#outline.
52
52
  #
53
+ # == Examples
54
+ #
53
55
  # Here is an example for creating an outline:
54
56
  #
55
57
  # doc = HexaPDF::Document.new
@@ -62,6 +64,22 @@ module HexaPDF
62
64
  # end
63
65
  # end
64
66
  #
67
+ # Here is one for copying the complete outline from one PDF to another:
68
+ #
69
+ # doc = HexaPDF::Document.open(ARGV[0])
70
+ # target = HexaPDF::Document.new
71
+ # stack = [target.outline]
72
+ # doc.outline.each_item do |item, level|
73
+ # if stack.size < level
74
+ # stack << stack.last[:Last]
75
+ # elsif stack.size > level
76
+ # (stack.size - level).times { stack.pop }
77
+ # end
78
+ # stack.last.add_item(target.import(item))
79
+ # end
80
+ # # Copying all the pages so that the references work.
81
+ # doc.pages.each {|page| target.pages << target.import(page) }
82
+ #
65
83
  # See: PDF1.7 s12.3.3
66
84
  class Outline < Dictionary
67
85
 
@@ -110,7 +128,7 @@ module HexaPDF
110
128
  node, dir = first ? [first, :Next] : [last, :Prev]
111
129
  node = node[dir] while node.key?(dir)
112
130
  self[dir == :Next ? :Last : :First] = node
113
- elsif !first && !last && self[:Count]
131
+ elsif !first && !last && self[:Count] && self[:Count] != 0
114
132
  yield('Outline dictionary key /Count set but no items exist', true)
115
133
  delete(:Count)
116
134
  end
@@ -126,6 +126,11 @@ module HexaPDF
126
126
  lister: "flags", getter: "flagged?", setter: "flag", unsetter: "unflag",
127
127
  value_getter: "self[:F]", value_setter: "self[:F]")
128
128
 
129
+ # Returns +true+ since outline items must always be indirect objects.
130
+ def must_be_indirect?
131
+ true
132
+ end
133
+
129
134
  # :call-seq:
130
135
  # item.title -> title
131
136
  # item.title(value) -> title
@@ -198,9 +203,46 @@ module HexaPDF
198
203
  end
199
204
  end
200
205
 
206
+ # Returns the outline level this item is one.
207
+ #
208
+ # The level of the items in the main outline dictionary, the root level, is 1.
209
+ #
210
+ # Here is an illustrated example of items contained in a document outline with their
211
+ # associated level:
212
+ #
213
+ # Outline dictionary 0
214
+ # Outline item 1 1
215
+ # |- Sub item 1 2
216
+ # |- Sub item 2 2
217
+ # |- Sub sub item 1 3
218
+ # |- Sub item 3 2
219
+ # Outline item 2 1
220
+ def level
221
+ count = 0
222
+ temp = self
223
+ count += 1 while (temp = temp[:Parent])
224
+ count
225
+ end
226
+
227
+ # Returns the destination page if there is any.
228
+ #
229
+ # * If a destination is set, the associated page is returned.
230
+ # * If an action is set and it is a GoTo action, the associated page is returned.
231
+ # * Otherwise +nil+ is returned.
232
+ def destination_page
233
+ dest = self[:Dest]
234
+ dest = action[:D] if !dest && (action = self[:A]) && action[:S] == :GoTo
235
+ document.destinations.resolve(dest)&.page
236
+ end
237
+
201
238
  # Adds, as child to this item, a new outline item with the given title that performs the
202
239
  # provided action on clicking. Returns the newly added item.
203
240
  #
241
+ # Alternatively, it is possible to provide an already initialized outline item instead of the
242
+ # title. If so, the only other argument that is used is +position+. Existing fields /Prev,
243
+ # /Next, /First, /Last, /Parent and /Count are deleted from the given item and set
244
+ # appropriately.
245
+ #
204
246
  # If neither :destination nor :action is specified, the outline item has no associated action.
205
247
  # This is only meaningful if the new item will have children as it then acts just as a
206
248
  # container.
@@ -251,16 +293,30 @@ module HexaPDF
251
293
  # end
252
294
  def add_item(title, destination: nil, action: nil, position: :last, open: true,
253
295
  text_color: nil, flags: nil) # :yield: item
254
- item = document.add({Parent: self}, type: :XXOutlineItem)
255
- item.title(title)
256
- if action
257
- item.action(action)
296
+ if title.kind_of?(HexaPDF::Object) && title.type == :XXOutlineItem
297
+ item = title
298
+ item.delete(:Prev)
299
+ item.delete(:Next)
300
+ item.delete(:First)
301
+ item.delete(:Last)
302
+ if item[:Count] && item[:Count] >= 0
303
+ item[:Count] = 0
304
+ else
305
+ item.delete(:Count)
306
+ end
307
+ item[:Parent] = self
258
308
  else
259
- item.destination(destination)
309
+ item = document.add({Parent: self}, type: :XXOutlineItem)
310
+ item.title(title)
311
+ if action
312
+ item.action(action)
313
+ else
314
+ item.destination(destination)
315
+ end
316
+ item.text_color(text_color) if text_color
317
+ item.flag(*flags) if flags
318
+ item[:Count] = 0 if open # Count=0 means open if items are later added
260
319
  end
261
- item.text_color(text_color) if text_color
262
- item.flag(*flags) if flags
263
- item[:Count] = 0 if open # Count=0 means open if items are later added
264
320
 
265
321
  unless position == :last || position == :first || position.kind_of?(Integer)
266
322
  raise ArgumentError, "position must be :first, :last, or an integer"
@@ -311,18 +367,20 @@ module HexaPDF
311
367
  end
312
368
 
313
369
  # :call-seq:
314
- # item.each_item {|descendant_item| block } -> item
315
- # item.each_item -> Enumerator
370
+ # item.each_item {|descendant_item, level| block } -> item
371
+ # item.each_item -> Enumerator
316
372
  #
317
373
  # Iterates over all descendant items of this one.
318
374
  #
319
- # The items are yielded in-order, yielding first the item itself and then its descendants.
375
+ # The descendant items are yielded in-order, yielding first the item itself and then its
376
+ # descendants.
320
377
  def each_item(&block)
321
378
  return to_enum(__method__) unless block_given?
379
+ return self unless (item = self[:First])
322
380
 
323
- item = self[:First]
381
+ level = self.level + 1
324
382
  while item
325
- yield(item)
383
+ yield(item, level)
326
384
  item.each_item(&block)
327
385
  item = item[:Next]
328
386
  end
@@ -341,7 +399,7 @@ module HexaPDF
341
399
  node, dir = first ? [first, :Next] : [last, :Prev]
342
400
  node = node[dir] while node.key?(dir)
343
401
  self[dir == :Next ? :Last : :First] = node
344
- elsif !first && !last && self[:Count] != 0
402
+ elsif !first && !last && self[:Count] && self[:Count] != 0
345
403
  yield('Outline item dictionary key /Count set but no descendants exist', true)
346
404
  delete(:Count)
347
405
  end