hexapdf 0.26.2 → 0.28.0

Sign up to get free protection for your applications and to get access to all the features.
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