hexapdf 0.26.2 → 0.28.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +115 -1
- data/README.md +1 -1
- data/examples/013-text_layouter_shapes.rb +8 -8
- data/examples/016-frame_automatic_box_placement.rb +3 -3
- data/examples/017-frame_text_flow.rb +3 -3
- data/examples/019-acro_form.rb +14 -3
- data/examples/020-column_box.rb +3 -3
- data/examples/023-images.rb +30 -0
- data/lib/hexapdf/cli/info.rb +5 -1
- data/lib/hexapdf/cli/inspect.rb +2 -2
- data/lib/hexapdf/cli/split.rb +8 -8
- data/lib/hexapdf/cli/watermark.rb +2 -2
- data/lib/hexapdf/configuration.rb +3 -2
- data/lib/hexapdf/content/canvas.rb +8 -3
- data/lib/hexapdf/dictionary.rb +4 -17
- data/lib/hexapdf/document/destinations.rb +42 -5
- data/lib/hexapdf/document/signatures.rb +265 -48
- data/lib/hexapdf/document.rb +6 -10
- data/lib/hexapdf/filter/ascii85_decode.rb +1 -1
- data/lib/hexapdf/importer.rb +35 -27
- data/lib/hexapdf/layout/list_box.rb +1 -5
- data/lib/hexapdf/object.rb +5 -0
- data/lib/hexapdf/parser.rb +14 -0
- data/lib/hexapdf/revision.rb +15 -12
- data/lib/hexapdf/revisions.rb +7 -1
- data/lib/hexapdf/tokenizer.rb +15 -9
- data/lib/hexapdf/type/acro_form/appearance_generator.rb +174 -128
- data/lib/hexapdf/type/acro_form/button_field.rb +5 -3
- data/lib/hexapdf/type/acro_form/choice_field.rb +2 -0
- data/lib/hexapdf/type/acro_form/field.rb +11 -5
- data/lib/hexapdf/type/acro_form/form.rb +61 -8
- data/lib/hexapdf/type/acro_form/signature_field.rb +2 -0
- data/lib/hexapdf/type/acro_form/text_field.rb +12 -2
- data/lib/hexapdf/type/annotations/widget.rb +3 -0
- data/lib/hexapdf/type/catalog.rb +1 -1
- data/lib/hexapdf/type/font_true_type.rb +14 -0
- data/lib/hexapdf/type/object_stream.rb +2 -2
- data/lib/hexapdf/type/outline.rb +19 -1
- data/lib/hexapdf/type/outline_item.rb +72 -14
- data/lib/hexapdf/type/page.rb +95 -64
- data/lib/hexapdf/type/resources.rb +13 -17
- data/lib/hexapdf/type/signature/adbe_pkcs7_detached.rb +16 -2
- data/lib/hexapdf/type/signature.rb +10 -0
- data/lib/hexapdf/version.rb +1 -1
- data/lib/hexapdf/writer.rb +5 -3
- data/test/hexapdf/content/test_canvas.rb +5 -0
- data/test/hexapdf/document/test_destinations.rb +41 -0
- data/test/hexapdf/document/test_pages.rb +2 -2
- data/test/hexapdf/document/test_signatures.rb +139 -19
- data/test/hexapdf/encryption/test_aes.rb +1 -1
- data/test/hexapdf/filter/test_predictor.rb +0 -1
- data/test/hexapdf/layout/test_box.rb +2 -1
- data/test/hexapdf/layout/test_column_box.rb +1 -1
- data/test/hexapdf/layout/test_list_box.rb +1 -1
- data/test/hexapdf/test_document.rb +2 -8
- data/test/hexapdf/test_importer.rb +27 -6
- data/test/hexapdf/test_parser.rb +19 -2
- data/test/hexapdf/test_revision.rb +15 -14
- data/test/hexapdf/test_revisions.rb +63 -12
- data/test/hexapdf/test_stream.rb +1 -1
- data/test/hexapdf/test_tokenizer.rb +10 -1
- data/test/hexapdf/test_writer.rb +11 -3
- data/test/hexapdf/type/acro_form/test_appearance_generator.rb +135 -56
- data/test/hexapdf/type/acro_form/test_button_field.rb +6 -1
- data/test/hexapdf/type/acro_form/test_choice_field.rb +4 -0
- data/test/hexapdf/type/acro_form/test_field.rb +4 -4
- data/test/hexapdf/type/acro_form/test_form.rb +65 -0
- data/test/hexapdf/type/acro_form/test_signature_field.rb +4 -0
- data/test/hexapdf/type/acro_form/test_text_field.rb +13 -0
- data/test/hexapdf/type/signature/common.rb +54 -0
- data/test/hexapdf/type/signature/test_adbe_pkcs7_detached.rb +21 -0
- data/test/hexapdf/type/test_catalog.rb +5 -2
- data/test/hexapdf/type/test_font_true_type.rb +20 -0
- data/test/hexapdf/type/test_object_stream.rb +2 -1
- data/test/hexapdf/type/test_outline.rb +4 -1
- data/test/hexapdf/type/test_outline_item.rb +62 -1
- data/test/hexapdf/type/test_page.rb +103 -45
- data/test/hexapdf/type/test_page_tree_node.rb +4 -2
- data/test/hexapdf/type/test_resources.rb +0 -5
- data/test/hexapdf/type/test_signature.rb +8 -0
- data/test/test_helper.rb +1 -1
- 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
|
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,
|
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,
|
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,
|
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
|
-
|
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
|
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
|
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(
|
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
|