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
@@ -10,6 +10,10 @@ describe HexaPDF::Type::AcroForm::ButtonField do
10
10
  @field = @doc.add({FT: :Btn, T: 'button'}, type: :XXAcroFormField, subtype: :Btn)
11
11
  end
12
12
 
13
+ it "identifies as an :XXAcroFormField type" do
14
+ assert_equal(:XXAcroFormField, @field.type)
15
+ end
16
+
13
17
  it "can be initialized as push button" do
14
18
  @field.initialize_as_push_button
15
19
  assert_nil(@field[:V])
@@ -232,6 +236,7 @@ describe HexaPDF::Type::AcroForm::ButtonField do
232
236
  @field.create_appearances
233
237
  yes = widget.appearance_dict.normal_appearance[:Yes]
234
238
  off = widget.appearance_dict.normal_appearance[:Off]
239
+ widget.appearance_dict.normal_appearance[:Yes] = HexaPDF::Reference.new(yes.oid)
235
240
  @field.create_appearances
236
241
  assert_same(yes, widget.appearance_dict.normal_appearance[:Yes])
237
242
  assert_same(off, widget.appearance_dict.normal_appearance[:Off])
@@ -252,7 +257,7 @@ describe HexaPDF::Type::AcroForm::ButtonField do
252
257
  refute_same(yes, widget.appearance_dict.normal_appearance[:Yes])
253
258
  end
254
259
 
255
- it "fails for unsupported button types" do
260
+ it "fails for push buttons as they are not implemented yet" do
256
261
  @field.flag(:push_button)
257
262
  @field.create_widget(@doc.pages.add, Rect: [0, 0, 0, 0])
258
263
  assert_raises(HexaPDF::Error) { @field.create_appearances }
@@ -10,6 +10,10 @@ describe HexaPDF::Type::AcroForm::ChoiceField do
10
10
  @field = @doc.add({FT: :Ch, T: 'choice'}, type: :XXAcroFormField, subtype: :Ch)
11
11
  end
12
12
 
13
+ it "identifies as an :XXAcroFormField type" do
14
+ assert_equal(:XXAcroFormField, @field.type)
15
+ end
16
+
13
17
  it "can be initialized as list box" do
14
18
  @field.initialize_as_list_box
15
19
  assert_nil(@field[:V])
@@ -111,14 +111,14 @@ describe HexaPDF::Type::AcroForm::Field do
111
111
  @field[:Subtype] = :Widget
112
112
  @field[:Rect] = [0, 0, 0, 0]
113
113
  widgets = @field.each_widget.to_a
114
- assert_kind_of(HexaPDF::Type::Annotations::Widget, *widgets)
114
+ assert_kind_of(HexaPDF::Type::Annotations::Widget, widgets.first)
115
115
  assert_same(@field.data, widgets.first.data)
116
116
  end
117
117
 
118
118
  it "yields all widgets in the /Kids array" do
119
119
  @field[:Kids] = [{Subtype: :Widget, Rect: [0, 0, 0, 0], X: 1}]
120
120
  widgets = @field.each_widget.to_a
121
- assert_kind_of(HexaPDF::Type::Annotations::Widget, *widgets)
121
+ assert_kind_of(HexaPDF::Type::Annotations::Widget, widgets.first)
122
122
  assert_equal(1, widgets.first[:X])
123
123
  end
124
124
 
@@ -128,8 +128,8 @@ describe HexaPDF::Type::AcroForm::Field do
128
128
  @doc.add({T: "b", Subtype: :Widget, Rect: [0, 0, 0, 0]}, type: :XXAcroFormField) <<
129
129
  @doc.add({T: "a", X: 1, Subtype: :Widget, Rect: [0, 0, 0, 0]}, type: :XXAcroFormField)
130
130
 
131
- widgets = @field.each_widget.to_a
132
- assert_kind_of(HexaPDF::Type::Annotations::Widget, *widgets)
131
+ widgets = @field.each_widget(direct_only: false).to_a
132
+ assert_kind_of(HexaPDF::Type::Annotations::Widget, widgets.first)
133
133
  assert_equal(1, widgets.first[:X])
134
134
  end
135
135
 
@@ -327,6 +327,12 @@ describe HexaPDF::Type::AcroForm::Form do
327
327
  assert_equal(1, result.size)
328
328
  assert(@doc.catalog.key?(:AcroForm))
329
329
  end
330
+
331
+ it "returns the fields that could not be flattened" do
332
+ @cb.create_appearances
333
+ result = @acro_form.flatten(create_appearances: false)
334
+ assert_equal([@tf], result)
335
+ end
330
336
  end
331
337
 
332
338
  describe "perform_validation" do
@@ -349,6 +355,65 @@ describe HexaPDF::Type::AcroForm::Form do
349
355
  assert_equal("0.0 g /F1 0 Tf", @acro_form[:DA])
350
356
  end
351
357
 
358
+ describe "field hierarchy validation" do
359
+ before do
360
+ @acro_form[:Fields] = [
361
+ nil,
362
+ HexaPDF::Object.new(nil),
363
+ 5,
364
+ HexaPDF::Object.new(5),
365
+ @doc.add({T: :Tx1}),
366
+ @doc.add({T: :Tx2, Kids: [nil, @doc.add({Subtype: :Widget})]}),
367
+ @doc.add({T: :Tx3, FT: :Tx, Kids: [@doc.add({T: :Tx4}),
368
+ [:nothing],
369
+ @doc.add({T: :Tx5, Kids: [@doc.add({T: :Tx6})]})]}),
370
+ ]
371
+ @acro_form[:Fields][6][:Kids][0][:Parent] = @acro_form[:Fields][6]
372
+ @acro_form[:Fields][6][:Kids][2][:Parent] = @acro_form[:Fields][6]
373
+ @acro_form[:Fields][6][:Kids][2][:Kids][0][:Parent] = @acro_form[:Fields][6][:Kids][2]
374
+ end
375
+
376
+ it "removes invalid objects from the field hierarchy" do
377
+ assert(@acro_form.validate)
378
+ assert_equal([:Tx1, :Tx2, :Tx3, :Tx4, :Tx5, :Tx6],
379
+ @acro_form.each_field(terminal_only: false).map {|f| f[:T] })
380
+ end
381
+
382
+ it "handles missing /Parent fields" do
383
+ @acro_form[:Fields][6][:Kids][0].delete(:Parent)
384
+ assert(@acro_form.validate)
385
+ assert_equal(1, @acro_form[:Fields][2][:Kids].size)
386
+ assert_equal(:Tx5, @acro_form[:Fields][2][:Kids][0][:T])
387
+ assert_equal(:Tx4, @acro_form[:Fields][3][:T])
388
+ end
389
+
390
+ it "handles /Parent field pointing to somewhere else" do
391
+ @acro_form[:Fields][6][:Kids][0][:Parent] = @acro_form[:Fields][4]
392
+ assert(@acro_form.validate)
393
+ assert_equal(2, @acro_form[:Fields][2][:Kids].size)
394
+ assert_equal(:Tx4, @acro_form[:Fields][2][:Kids][0][:T])
395
+ assert_equal(@acro_form[:Fields][2], @acro_form[:Fields][2][:Kids][0][:Parent])
396
+ end
397
+ end
398
+
399
+ describe "combining fields with the same name" do
400
+ before do
401
+ @acro_form[:Fields] = [
402
+ @doc.add({T: 'e', Subtype: :Widget, Rect: [0, 0, 0, 1]}),
403
+ @doc.add({T: 'e', Subtype: :Widget, Rect: [0, 0, 0, 2]}),
404
+ @doc.add({T: 'Tx2'}),
405
+ @doc.add({T: 'e', Kids: [{Subtype: :Widget, Rect: [0, 0, 0, 3]}]}),
406
+ ]
407
+ end
408
+
409
+ it "merges fields with the same name into the first one" do
410
+ assert(@acro_form.validate)
411
+ assert_equal(2, @acro_form.root_fields.size)
412
+ assert_equal([[0, 0, 0, 1], [0, 0, 0, 2], [0, 0, 0, 3]],
413
+ @acro_form.field_by_name('e').each_widget.map {|w| w[:Rect] })
414
+ end
415
+ end
416
+
352
417
  describe "automatically creates the terminal fields; appearances" do
353
418
  before do
354
419
  @cb = @acro_form.create_check_box('test2')
@@ -20,6 +20,10 @@ describe HexaPDF::Type::AcroForm::SignatureField do
20
20
  @field = @doc.wrap({}, type: :XXAcroFormField, subtype: :Sig)
21
21
  end
22
22
 
23
+ it "identifies as an :XXAcroFormField type" do
24
+ assert_equal(:XXAcroFormField, @field.type)
25
+ end
26
+
23
27
  it "sets the field value" do
24
28
  @field.field_value = {Empty: :True}
25
29
  assert_equal({Empty: :True}, @field[:V].value)
@@ -10,6 +10,10 @@ describe HexaPDF::Type::AcroForm::TextField do
10
10
  @field = @doc.add({FT: :Tx}, type: :XXAcroFormField, subtype: :Tx)
11
11
  end
12
12
 
13
+ it "identifies as an :XXAcroFormField type" do
14
+ assert_equal(:XXAcroFormField, @field.type)
15
+ end
16
+
13
17
  it "resolves /MaxLen as inheritable field" do
14
18
  assert_nil(@field[:MaxLen])
15
19
 
@@ -164,11 +168,20 @@ describe HexaPDF::Type::AcroForm::TextField do
164
168
  assert_same(stream, @field[:AP][:N].raw_stream)
165
169
  @field.field_value = 'test'
166
170
  refute_same(stream, @field[:AP][:N].raw_stream)
171
+ stream = @field[:AP][:N].raw_stream
167
172
 
168
173
  widget = @field.create_widget(@doc.pages.add, Rect: [0, 0, 0, 0])
169
174
  assert_nil(widget[:AP])
170
175
  @field.create_appearances
171
176
  refute_nil(widget[:AP][:N])
177
+
178
+ @doc.clear_cache
179
+ @field.create_appearances
180
+ assert_same(stream, @field[:Kids][0][:AP][:N].raw_stream)
181
+
182
+ @doc.clear_cache
183
+ @field.field_value = 'other'
184
+ refute_same(stream, @field[:Kids][0][:AP][:N].raw_stream)
172
185
  end
173
186
 
174
187
  it "always creates a new appearance stream if force is true" do
@@ -65,6 +65,60 @@ module HexaPDF
65
65
  end
66
66
  end
67
67
 
68
+ def timestamp_certificate
69
+ @timestamp_certificate ||=
70
+ begin
71
+ name = OpenSSL::X509::Name.parse('/CN=timestamp/DC=gettalong')
72
+
73
+ signer_cert = OpenSSL::X509::Certificate.new
74
+ signer_cert.serial = 3
75
+ signer_cert.version = 2
76
+ signer_cert.not_before = Time.now - 86400
77
+ signer_cert.not_after = Time.now + 86400
78
+ signer_cert.public_key = signer_key.public_key
79
+ signer_cert.subject = name
80
+ signer_cert.issuer = ca_certificate.subject
81
+
82
+ extension_factory = OpenSSL::X509::ExtensionFactory.new
83
+ extension_factory.subject_certificate = signer_cert
84
+ extension_factory.issuer_certificate = ca_certificate
85
+ signer_cert.add_extension(extension_factory.create_extension('subjectKeyIdentifier', 'hash'))
86
+ signer_cert.add_extension(extension_factory.create_extension('basicConstraints', 'CA:FALSE'))
87
+ signer_cert.add_extension(extension_factory.create_extension('keyUsage', 'digitalSignature'))
88
+ signer_cert.add_extension(extension_factory.create_extension('extendedKeyUsage',
89
+ 'timeStamping', true))
90
+ signer_cert.sign(ca_key, OpenSSL::Digest.new('SHA1'))
91
+
92
+ signer_cert
93
+ end
94
+ end
95
+
96
+ def start_tsa_server
97
+ return if defined?(@tsa_server)
98
+ require 'webrick'
99
+ port = 34567
100
+ @tsa_server = WEBrick::HTTPServer.new(Port: port, BindAddress: '127.0.0.1',
101
+ Logger: WEBrick::Log.new(StringIO.new), AccessLog: [])
102
+ @tsa_server.mount_proc('/') do |request, response|
103
+ @tsr = OpenSSL::Timestamp::Request.new(request.body)
104
+ case (@tsr.policy_id || '1.2.3.4.0')
105
+ when '1.2.3.4.0', '1.2.3.4.2'
106
+ fac = OpenSSL::Timestamp::Factory.new
107
+ fac.gen_time = Time.now
108
+ fac.serial_number = 1
109
+ fac.default_policy_id = '1.2.3.4.5'
110
+ fac.allowed_digests = ["sha256", "sha512"]
111
+ tsr = fac.create_timestamp(CERTIFICATES.signer_key, CERTIFICATES.timestamp_certificate,
112
+ @tsr)
113
+ response.body = tsr.to_der
114
+ when '1.2.3.4.1'
115
+ response.status = 403
116
+ response.body = "Invalid"
117
+ end
118
+ end
119
+ Thread.new { @tsa_server.start }
120
+ end
121
+
68
122
  end
69
123
 
70
124
  end
@@ -1,5 +1,6 @@
1
1
  # -*- encoding: utf-8 -*-
2
2
 
3
+ require 'digest'
3
4
  require 'test_helper'
4
5
  require_relative 'common'
5
6
  require 'hexapdf/type/signature'
@@ -95,5 +96,25 @@ describe HexaPDF::Type::Signature::AdbePkcs7Detached do
95
96
  assert_equal(:error, result.messages.last.type)
96
97
  assert_match(/Signature verification failed/, result.messages.last.content)
97
98
  end
99
+
100
+ it "verifies a timestamp signature" do
101
+ req = OpenSSL::Timestamp::Request.new
102
+ req.algorithm = 'SHA256'
103
+ req.message_imprint = Digest::SHA256.digest(@data)
104
+ req.policy_id = "1.2.3.4.5"
105
+ req.nonce = 42
106
+ fac = OpenSSL::Timestamp::Factory.new
107
+ fac.gen_time = Time.now
108
+ fac.serial_number = 1
109
+ fac.allowed_digests = ["sha256", "sha512"]
110
+ res = fac.create_timestamp(CERTIFICATES.signer_key, CERTIFICATES.timestamp_certificate, req)
111
+ @dict.contents = res.token.to_der
112
+ @dict.signature_type = 'ETSI.RFC3161'
113
+ @handler = HexaPDF::Type::Signature::AdbePkcs7Detached.new(@dict)
114
+
115
+ result = @handler.verify(@store)
116
+ assert_equal(:info, result.messages.last.type)
117
+ assert_match(/Signature valid/, result.messages.last.content)
118
+ end
98
119
  end
99
120
  end
@@ -29,8 +29,11 @@ describe HexaPDF::Type::Catalog do
29
29
  assert_same(other, names)
30
30
  end
31
31
 
32
- it "creates the document outline on access" do
33
- assert_nil(@catalog[:Outlines])
32
+ it "uses or creates the document outline on access" do
33
+ @catalog[:Outlines] = {}
34
+ assert_equal(:Outlines, @catalog.outline.type)
35
+
36
+ @catalog.delete(:Outlines)
34
37
  outline = @catalog.outline
35
38
  assert_equal(:Outlines, outline.type)
36
39
  assert_same(outline, @catalog.outline)
@@ -15,6 +15,26 @@ describe HexaPDF::Type::FontTrueType do
15
15
  BaseFont: :Something, FontDescriptor: font_descriptor})
16
16
  end
17
17
 
18
+ describe "font_wrapper" do
19
+ it "returns the default value if the font is subset" do
20
+ @font[:BaseFont] = :'ABCDEF+Something'
21
+ assert_nil(@font.font_wrapper)
22
+ end
23
+
24
+ it "returns the default value if the font has no embedded font file" do
25
+ assert_nil(@font.font_wrapper)
26
+ end
27
+
28
+ it "uses a fully embedded TrueType font file" do
29
+ font_file = File.binread(File.join(TEST_DATA_DIR, "fonts", "Ubuntu-Title.ttf"))
30
+ @font[:FontDescriptor][:FontFile2] = @doc.add({}, stream: font_file)
31
+ font_wrapper = @font.font_wrapper
32
+ assert(font_wrapper)
33
+ assert_equal(font_file, font_wrapper.wrapped_font.io.string)
34
+ assert_same(font_wrapper, @font.font_wrapper)
35
+ end
36
+ end
37
+
18
38
  describe "validation" do
19
39
  it "ignores some missing fields if the font name is one of the standard PDF fonts" do
20
40
  @font[:BaseFont] = :'Arial,Bold'
@@ -104,7 +104,8 @@ describe HexaPDF::Type::ObjectStream do
104
104
  assert_equal("", @obj.stream)
105
105
  end
106
106
 
107
- it "doesn't allow the Catalog entry to be compressed when encryption is used" do
107
+ it "doesn't allow the Catalog entry to be compressed" do
108
+ @doc.trailer.delete(:Encrypt)
108
109
  @obj.add_object(HexaPDF::Dictionary.new({Type: :Catalog}, oid: 8))
109
110
  @obj.write_objects(@revision)
110
111
  assert_equal(0, @obj.value[:N])
@@ -25,7 +25,7 @@ describe HexaPDF::Type::Outline do
25
25
  end
26
26
  item1.add_item("Item5")
27
27
  end
28
- assert_equal(%w[Item1 Item2 Item3 Item4 Item5], @outline.each_item.map(&:title))
28
+ assert_equal(%w[Item1 Item2 Item3 Item4 Item5], @outline.each_item.map {|i, _| i.title })
29
29
  end
30
30
 
31
31
  describe "perform_validation" do
@@ -64,6 +64,9 @@ describe HexaPDF::Type::Outline do
64
64
  assert(correctable)
65
65
  end
66
66
  refute(@outline.key?(:Count))
67
+
68
+ @outline[:Count] = 0
69
+ assert(@outline.validate(auto_correct: false))
67
70
  end
68
71
  end
69
72
  end
@@ -10,6 +10,10 @@ describe HexaPDF::Type::OutlineItem do
10
10
  @item = @doc.add({Title: "root", Count: 0}, type: :XXOutlineItem)
11
11
  end
12
12
 
13
+ it "must be an indirect object" do
14
+ assert(@item.must_be_indirect?)
15
+ end
16
+
13
17
  describe "title" do
14
18
  it "returns the set title" do
15
19
  @item[:Title] = 'Test'
@@ -78,6 +82,43 @@ describe HexaPDF::Type::OutlineItem do
78
82
  end
79
83
  end
80
84
 
85
+ describe "level" do
86
+ it "returns 0 for the outline dictionary when treated as an item" do
87
+ assert_equal(0, @item.level)
88
+ end
89
+
90
+ it "returns 1 for the root level items" do
91
+ @item[:Parent] = {Type: :Outlines}
92
+ assert_equal(1, @item.level)
93
+ end
94
+
95
+ it "returns the correct level for items in the hierarchy" do
96
+ @item[:Parent] = {Title: 'Root elem', Parent: {Type: :Outlines}}
97
+ assert_equal(2, @item.level)
98
+ end
99
+ end
100
+
101
+ describe "destination_page" do
102
+ it "returns the page of a set destination" do
103
+ @item[:Dest] = [5, :Fit]
104
+ assert_equal(5, @item.destination_page)
105
+ end
106
+
107
+ it "returns the page of a set GoTO action" do
108
+ @item[:A] = {S: :GoTo, D: [5, :Fit]}
109
+ assert_equal(5, @item.destination_page)
110
+ end
111
+
112
+ it "returns nil if no destination or action is set" do
113
+ assert_nil(@item.destination_page)
114
+ end
115
+
116
+ it "returns nil if an action besides GoTo is set" do
117
+ @item[:A] = {S: :GoToR}
118
+ assert_nil(@item.destination_page)
119
+ end
120
+ end
121
+
81
122
  describe "add" do
82
123
  it "returns the created item" do
83
124
  new_item = @item.add_item("Test")
@@ -118,6 +159,24 @@ describe HexaPDF::Type::OutlineItem do
118
159
  assert_same(new_item, yielded_item)
119
160
  end
120
161
 
162
+ it "uses the provided outline item instead of creating a new one" do
163
+ item = @doc.wrap({Dest: [1, :Fit], flags: 1, First: 5, Count: 2}, type: :XXOutlineItem)
164
+ new_item = @item.add_item(item, destination: [2, :Fit])
165
+ assert_same(item, new_item)
166
+ assert_equal([1, :Fit], new_item.destination)
167
+ assert_same(@item, new_item[:Parent])
168
+ refute(new_item.key?(:First))
169
+ assert_equal(0, new_item[:Count])
170
+
171
+ item = @doc.wrap({Count: nil}, type: :XXOutlineItem)
172
+ new_item = @item.add_item(item)
173
+ refute(new_item.key?(:Count))
174
+
175
+ item = @doc.wrap({Count: -1}, type: :XXOutlineItem)
176
+ new_item = @item.add_item(item)
177
+ refute(new_item.key?(:Count))
178
+ end
179
+
121
180
  describe "position" do
122
181
  it "works for an empty item" do
123
182
  new_item = @item.add_item("Test")
@@ -211,7 +270,8 @@ describe HexaPDF::Type::OutlineItem do
211
270
  end
212
271
  item1.add_item("Item5")
213
272
  end
214
- assert_equal(%w[Item1 Item2 Item3 Item4 Item5], @item.each_item.map(&:title))
273
+ assert_equal(['Item1', 1, 'Item2', 2, 'Item3', 2, 'Item4', 3, 'Item5', 2],
274
+ @item.each_item.map {|i, l| [i.title, l] }.flatten)
215
275
  end
216
276
 
217
277
  describe "perform_validation" do
@@ -251,6 +311,7 @@ describe HexaPDF::Type::OutlineItem do
251
311
  assert(correctable)
252
312
  end
253
313
  refute(@item.key?(:Count))
314
+ assert(@item.validate(auto_correct: false))
254
315
  end
255
316
 
256
317
  it "fails validation if the previous item's /Next points somewhere else" do