hexapdf 0.26.2 → 0.27.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +56 -0
- 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/020-column_box.rb +3 -3
- data/lib/hexapdf/cli/split.rb +7 -7
- data/lib/hexapdf/cli/watermark.rb +2 -2
- data/lib/hexapdf/configuration.rb +2 -0
- data/lib/hexapdf/dictionary.rb +3 -12
- data/lib/hexapdf/document/destinations.rb +42 -5
- data/lib/hexapdf/document/signatures.rb +265 -48
- data/lib/hexapdf/importer.rb +3 -0
- data/lib/hexapdf/parser.rb +1 -0
- data/lib/hexapdf/revisions.rb +3 -1
- data/lib/hexapdf/tokenizer.rb +2 -2
- data/lib/hexapdf/type/acro_form/form.rb +28 -1
- data/lib/hexapdf/type/catalog.rb +1 -1
- data/lib/hexapdf/type/outline.rb +18 -0
- data/lib/hexapdf/type/outline_item.rb +72 -14
- data/lib/hexapdf/type/page.rb +56 -35
- 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 +3 -0
- data/test/hexapdf/document/test_destinations.rb +41 -0
- data/test/hexapdf/document/test_signatures.rb +139 -19
- data/test/hexapdf/test_importer.rb +14 -0
- data/test/hexapdf/test_parser.rb +2 -2
- data/test/hexapdf/test_revisions.rb +20 -12
- data/test/hexapdf/test_tokenizer.rb +11 -1
- data/test/hexapdf/test_writer.rb +11 -3
- data/test/hexapdf/type/acro_form/test_form.rb +47 -0
- data/test/hexapdf/type/signature/common.rb +52 -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_outline.rb +1 -1
- data/test/hexapdf/type/test_outline_item.rb +62 -1
- data/test/hexapdf/type/test_page.rb +41 -20
- 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 +17 -3
@@ -19,6 +19,7 @@ describe HexaPDF::Importer::NullableWeakRef do
|
|
19
19
|
end
|
20
20
|
|
21
21
|
describe HexaPDF::Importer do
|
22
|
+
class TestClass < HexaPDF::Dictionary; end
|
22
23
|
before do
|
23
24
|
@source = HexaPDF::Document.new
|
24
25
|
obj = @source.add("test")
|
@@ -86,6 +87,19 @@ describe HexaPDF::Importer do
|
|
86
87
|
assert_same(hash, obj[:hash])
|
87
88
|
end
|
88
89
|
|
90
|
+
it "uses the class of the argument when directly importing a HexaPDF::Object" do
|
91
|
+
src_obj = @source.wrap(@hash, type: TestClass)
|
92
|
+
dest_obj = @importer.import(src_obj)
|
93
|
+
assert_instance_of(TestClass, dest_obj)
|
94
|
+
end
|
95
|
+
|
96
|
+
it "uses the class of the argument when importing an already mapped HexaPDF::Object" do
|
97
|
+
@importer.import(@obj) # also maps @hash
|
98
|
+
src_obj = @source.wrap(@hash, type: TestClass)
|
99
|
+
dest_obj = @importer.import(src_obj)
|
100
|
+
assert_instance_of(TestClass, dest_obj)
|
101
|
+
end
|
102
|
+
|
89
103
|
it "duplicates the stream if it is a string" do
|
90
104
|
src_obj = @source.add({}, stream: 'data')
|
91
105
|
dst_obj = @importer.import(src_obj)
|
data/test/hexapdf/test_parser.rb
CHANGED
@@ -108,7 +108,7 @@ describe HexaPDF::Parser do
|
|
108
108
|
end
|
109
109
|
|
110
110
|
it "treats indirect objects with invalid values as null objects" do
|
111
|
-
create_parser("1 0 obj <</test
|
111
|
+
create_parser("1 0 obj <</test <end)> > endobj")
|
112
112
|
object, * = @parser.parse_indirect_object
|
113
113
|
assert_nil(object)
|
114
114
|
end
|
@@ -210,7 +210,7 @@ describe HexaPDF::Parser do
|
|
210
210
|
end
|
211
211
|
|
212
212
|
it "fails for invalid values" do
|
213
|
-
create_parser("1 0 obj <</test
|
213
|
+
create_parser("1 0 obj <</test <end)> >endobj")
|
214
214
|
exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.parse_indirect_object }
|
215
215
|
assert_match(/Invalid value after '1 0 obj'/, exp.message)
|
216
216
|
end
|
@@ -273,40 +273,48 @@ describe HexaPDF::Revisions do
|
|
273
273
|
1 0 obj
|
274
274
|
10
|
275
275
|
endobj
|
276
|
+
xref
|
277
|
+
0 2
|
278
|
+
0000000000 65535 f
|
279
|
+
0000000009 00000 n
|
280
|
+
trailer
|
281
|
+
<< /Size 2 >>
|
282
|
+
startxref
|
283
|
+
27
|
284
|
+
%%EOF
|
276
285
|
|
277
286
|
2 0 obj
|
278
287
|
20
|
279
288
|
endobj
|
280
289
|
|
281
290
|
3 0 obj
|
282
|
-
<< /Type /XRef /Size
|
291
|
+
<< /Type /XRef /Size 4 /Index [2 1] /W [1 1 1] /Filter /ASCIIHexDecode /Length 6
|
283
292
|
>>stream
|
284
|
-
|
293
|
+
017600
|
285
294
|
endstream
|
286
295
|
endobj
|
287
296
|
|
288
297
|
xref
|
289
|
-
|
290
|
-
0000000000 65535 f
|
291
|
-
0000000009 00000 n
|
298
|
+
2 2
|
292
299
|
0000000000 65535 f
|
293
|
-
|
300
|
+
0000000137 00000 n
|
294
301
|
trailer
|
295
|
-
<< /Size
|
302
|
+
<< /Size 4 /Prev 27>>
|
296
303
|
startxref
|
297
|
-
|
304
|
+
260
|
298
305
|
%%EOF
|
299
306
|
|
300
307
|
xref
|
301
308
|
0 0
|
302
309
|
trailer
|
303
|
-
<< /Size
|
310
|
+
<< /Size 4 /Prev 260 /XRefStm 137>>
|
304
311
|
startxref
|
305
|
-
|
312
|
+
360
|
306
313
|
%%EOF
|
307
314
|
EOF
|
308
|
-
doc = HexaPDF::Document.new(io: io)
|
309
|
-
assert_equal(
|
315
|
+
doc = HexaPDF::Document.new(io: io, config: {'parser.try_xref_reconstruction' => false})
|
316
|
+
assert_equal(2, doc.revisions.count)
|
317
|
+
assert_equal(10, doc.object(1).value)
|
310
318
|
assert_equal(20, doc.object(2).value)
|
311
319
|
end
|
312
320
|
|
@@ -13,9 +13,19 @@ describe HexaPDF::Tokenizer do
|
|
13
13
|
end
|
14
14
|
|
15
15
|
it "handles object references" do
|
16
|
-
|
16
|
+
#HexaPDF::Reference.new(1, 0), HexaPDF::Reference.new(1, 2), 2, -1, 'R', 0, 0, 'R', -1, 0, 'R',
|
17
|
+
create_tokenizer("1 0 R +2 +15 R 2 -1 R 0 0 R -1 0 R")
|
17
18
|
assert_equal(HexaPDF::Reference.new(1, 0), @tokenizer.next_token)
|
18
19
|
assert_equal(HexaPDF::Reference.new(2, 15), @tokenizer.next_token)
|
20
|
+
assert_equal(2, @tokenizer.next_token)
|
21
|
+
assert_equal(-1, @tokenizer.next_token)
|
22
|
+
assert_equal('R', @tokenizer.next_token)
|
23
|
+
assert_equal(0, @tokenizer.next_token)
|
24
|
+
assert_equal(0, @tokenizer.next_token)
|
25
|
+
assert_equal('R', @tokenizer.next_token)
|
26
|
+
assert_equal(-1, @tokenizer.next_token)
|
27
|
+
assert_equal(0, @tokenizer.next_token)
|
28
|
+
assert_equal('R', @tokenizer.next_token)
|
19
29
|
@tokenizer.pos = 0
|
20
30
|
assert_equal(HexaPDF::Reference.new(1, 0), @tokenizer.next_object)
|
21
31
|
assert_equal(HexaPDF::Reference.new(2, 15), @tokenizer.next_object)
|
data/test/hexapdf/test_writer.rb
CHANGED
@@ -40,7 +40,7 @@ describe HexaPDF::Writer do
|
|
40
40
|
219
|
41
41
|
%%EOF
|
42
42
|
3 0 obj
|
43
|
-
<</Producer(HexaPDF version 0.
|
43
|
+
<</Producer(HexaPDF version 0.27.0)>>
|
44
44
|
endobj
|
45
45
|
xref
|
46
46
|
3 1
|
@@ -72,7 +72,7 @@ describe HexaPDF::Writer do
|
|
72
72
|
141
|
73
73
|
%%EOF
|
74
74
|
6 0 obj
|
75
|
-
<</Producer(HexaPDF version 0.
|
75
|
+
<</Producer(HexaPDF version 0.27.0)>>
|
76
76
|
endobj
|
77
77
|
2 0 obj
|
78
78
|
<</Length 10>>stream
|
@@ -125,6 +125,14 @@ describe HexaPDF::Writer do
|
|
125
125
|
refute_match(/^trailer/, output_io.string)
|
126
126
|
end
|
127
127
|
|
128
|
+
it "updates the PDF version using the catalog's /Version entry if necessary" do
|
129
|
+
doc = HexaPDF::Document.new(io: @std_input_io)
|
130
|
+
doc.version = '2.0'
|
131
|
+
output_io = StringIO.new
|
132
|
+
HexaPDF::Writer.write(doc, output_io, incremental: true)
|
133
|
+
assert_equal('2.0', HexaPDF::Document.new(io: output_io).version)
|
134
|
+
end
|
135
|
+
|
128
136
|
it "raises an error if the used encryption was changed" do
|
129
137
|
io = StringIO.new
|
130
138
|
doc = HexaPDF::Document.new
|
@@ -206,7 +214,7 @@ describe HexaPDF::Writer do
|
|
206
214
|
<</Type/Page/MediaBox[0 0 595 842]/Parent 2 0 R/Resources<<>>>>
|
207
215
|
endobj
|
208
216
|
5 0 obj
|
209
|
-
<</Producer(HexaPDF version 0.
|
217
|
+
<</Producer(HexaPDF version 0.27.0)>>
|
210
218
|
endobj
|
211
219
|
4 0 obj
|
212
220
|
<</Root 1 0 R/Info 5 0 R/Size 6/Type/XRef/W[1 1 2]/Index[0 6]/Filter/FlateDecode/DecodeParms<</Columns 4/Predictor 12>>/Length 33>>stream
|
@@ -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,47 @@ 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
|
+
|
352
399
|
describe "automatically creates the terminal fields; appearances" do
|
353
400
|
before do
|
354
401
|
@cb = @acro_form.create_check_box('test2')
|
@@ -65,6 +65,58 @@ 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', 'timeStamping', true))
|
89
|
+
signer_cert.sign(ca_key, OpenSSL::Digest.new('SHA1'))
|
90
|
+
|
91
|
+
signer_cert
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def start_tsa_server
|
96
|
+
return if defined?(@tsa_server)
|
97
|
+
require 'webrick'
|
98
|
+
port = 34567
|
99
|
+
@tsa_server = WEBrick::HTTPServer.new(Port: port, BindAddress: '127.0.0.1',
|
100
|
+
Logger: WEBrick::Log.new(StringIO.new), AccessLog: [])
|
101
|
+
@tsa_server.mount_proc('/') do |request, response|
|
102
|
+
@tsr = OpenSSL::Timestamp::Request.new(request.body)
|
103
|
+
case (@tsr.policy_id || '1.2.3.4.0')
|
104
|
+
when '1.2.3.4.0', '1.2.3.4.2'
|
105
|
+
fac = OpenSSL::Timestamp::Factory.new
|
106
|
+
fac.gen_time = Time.now
|
107
|
+
fac.serial_number = 1
|
108
|
+
fac.default_policy_id = '1.2.3.4.5'
|
109
|
+
fac.allowed_digests = ["sha256", "sha512"]
|
110
|
+
tsr = fac.create_timestamp(CERTIFICATES.signer_key, CERTIFICATES.timestamp_certificate,
|
111
|
+
@tsr)
|
112
|
+
response.body = tsr.to_der
|
113
|
+
when '1.2.3.4.1'
|
114
|
+
response.status = 403
|
115
|
+
response.body = "Invalid"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
Thread.new { @tsa_server.start }
|
119
|
+
end
|
68
120
|
end
|
69
121
|
|
70
122
|
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)
|
@@ -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
|
@@ -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
|
@@ -94,6 +94,11 @@ describe HexaPDF::Type::Page do
|
|
94
94
|
@page = @doc.pages.add
|
95
95
|
end
|
96
96
|
|
97
|
+
it "returns the crop box by default" do
|
98
|
+
@page[:CropBox] = [0, 0, 100, 200]
|
99
|
+
assert_equal([0, 0, 100, 200], @page.box)
|
100
|
+
end
|
101
|
+
|
97
102
|
it "returns the correct media box" do
|
98
103
|
@page[:MediaBox] = :media
|
99
104
|
assert_equal(:media, @page.box(:media))
|
@@ -150,15 +155,25 @@ describe HexaPDF::Type::Page do
|
|
150
155
|
@page = @doc.pages.add
|
151
156
|
end
|
152
157
|
|
153
|
-
it "
|
154
|
-
@page
|
158
|
+
it "uses the crop box by default" do
|
159
|
+
@page[:CropBox] = [0, 0, 200, 100]
|
160
|
+
assert_equal(:landscape, @page.orientation)
|
161
|
+
end
|
162
|
+
|
163
|
+
it "uses the specified box for determining the orientation" do
|
164
|
+
@page[:ArtBox] = [0, 0, 200, 100]
|
165
|
+
assert_equal(:landscape, @page.orientation(:art))
|
166
|
+
end
|
167
|
+
|
168
|
+
it "returns :portrait for appropriate boxes and rotation values" do
|
169
|
+
@page.box(:crop, [0, 0, 100, 300])
|
155
170
|
assert_equal(:portrait, @page.orientation)
|
156
171
|
@page[:Rotate] = 0
|
157
172
|
assert_equal(:portrait, @page.orientation)
|
158
173
|
@page[:Rotate] = 180
|
159
174
|
assert_equal(:portrait, @page.orientation)
|
160
175
|
|
161
|
-
@page.box(:
|
176
|
+
@page.box(:crop, [0, 0, 300, 100])
|
162
177
|
@page[:Rotate] = 90
|
163
178
|
assert_equal(:portrait, @page.orientation)
|
164
179
|
@page[:Rotate] = 270
|
@@ -166,14 +181,14 @@ describe HexaPDF::Type::Page do
|
|
166
181
|
end
|
167
182
|
|
168
183
|
it "returns :landscape for appropriate media boxes and rotation values" do
|
169
|
-
@page.box(:
|
184
|
+
@page.box(:crop, [0, 0, 300, 100])
|
170
185
|
assert_equal(:landscape, @page.orientation)
|
171
186
|
@page[:Rotate] = 0
|
172
187
|
assert_equal(:landscape, @page.orientation)
|
173
188
|
@page[:Rotate] = 180
|
174
189
|
assert_equal(:landscape, @page.orientation)
|
175
190
|
|
176
|
-
@page.box(:
|
191
|
+
@page.box(:crop, [0, 0, 100, 300])
|
177
192
|
@page[:Rotate] = 90
|
178
193
|
assert_equal(:landscape, @page.orientation)
|
179
194
|
@page[:Rotate] = 270
|
@@ -204,18 +219,17 @@ describe HexaPDF::Type::Page do
|
|
204
219
|
|
205
220
|
describe "flatten" do
|
206
221
|
it "adjust all page boxes" do
|
207
|
-
@page.box(:crop,
|
208
|
-
@page.box(:bleed,
|
209
|
-
@page.box(:trim,
|
210
|
-
@page.box(:art,
|
222
|
+
@page.box(:crop, [0, 0, 1, 2])
|
223
|
+
@page.box(:bleed, [0, 0, 2, 3])
|
224
|
+
@page.box(:trim, [0, 0, 3, 4])
|
225
|
+
@page.box(:art, [0, 0, 4, 5])
|
211
226
|
|
212
227
|
@page.rotate(90, flatten: true)
|
213
|
-
|
214
|
-
assert_equal(
|
215
|
-
assert_equal(
|
216
|
-
assert_equal(
|
217
|
-
assert_equal(
|
218
|
-
assert_equal(box, @page.box(:art).value)
|
228
|
+
assert_equal([-300, 50, -100, 200], @page.box(:media).value)
|
229
|
+
assert_equal([-2, 0, 0, 1], @page.box(:crop).value)
|
230
|
+
assert_equal([-3, 0, 0, 2], @page.box(:bleed).value)
|
231
|
+
assert_equal([-4, 0, 0, 3], @page.box(:trim).value)
|
232
|
+
assert_equal([-5, 0, 0, 4], @page.box(:art).value)
|
219
233
|
end
|
220
234
|
|
221
235
|
it "works correctly for 90 degrees" do
|
@@ -405,8 +419,8 @@ describe HexaPDF::Type::Page do
|
|
405
419
|
[:restore_graphics_state]])
|
406
420
|
end
|
407
421
|
|
408
|
-
it "works correctly if the page has its origin not at (0,0)" do
|
409
|
-
@page.box(:
|
422
|
+
it "works correctly if the page has its crop box origin not at (0,0)" do
|
423
|
+
@page.box(:crop, [-10, -5, 100, 300])
|
410
424
|
@page.canvas(type: :underlay).line_width = 2
|
411
425
|
@page.canvas(type: :page).line_width = 2
|
412
426
|
@page.canvas(type: :overlay).line_width = 2
|
@@ -427,6 +441,13 @@ describe HexaPDF::Type::Page do
|
|
427
441
|
[:restore_graphics_state]])
|
428
442
|
end
|
429
443
|
|
444
|
+
it "allows disabling the origin translation" do
|
445
|
+
@page.box(:crop, [-10, -5, 100, 300])
|
446
|
+
@page.canvas(translate_origin: false).line_width = 2
|
447
|
+
|
448
|
+
assert_page_operators(@page, [[:set_line_width, [2]]])
|
449
|
+
end
|
450
|
+
|
430
451
|
it "fails if the page canvas is requested for a page with existing contents" do
|
431
452
|
@page.contents = "q Q"
|
432
453
|
assert_raises(HexaPDF::Error) { @page.canvas }
|
@@ -554,10 +575,10 @@ describe HexaPDF::Type::Page do
|
|
554
575
|
[:restore_graphics_state]])
|
555
576
|
end
|
556
577
|
|
557
|
-
it "adjusts the
|
558
|
-
@
|
578
|
+
it "potentially adjusts the origin so that it is always in (0,0)" do
|
579
|
+
@canvas.translate(-15, -15)
|
559
580
|
@page.flatten_annotations
|
560
|
-
assert_operators(@canvas.contents, [:concatenate_matrix, [1, 0, 0, 1, 15, 15]], range:
|
581
|
+
assert_operators(@canvas.contents, [:concatenate_matrix, [1, 0, 0, 1, 15, 15]], range: 2)
|
561
582
|
end
|
562
583
|
|
563
584
|
it "adjusts the position in case the form /Matrix has an offset" do
|
@@ -189,11 +189,6 @@ describe HexaPDF::Type::Resources do
|
|
189
189
|
end
|
190
190
|
|
191
191
|
describe "validation" do
|
192
|
-
it "assigns the default value if ProcSet is not set" do
|
193
|
-
@res.validate
|
194
|
-
assert_equal([:PDF, :Text, :ImageB, :ImageC, :ImageI], @res[:ProcSet].value)
|
195
|
-
end
|
196
|
-
|
197
192
|
it "handles an invalid ProcSet containing a single value instead of an array" do
|
198
193
|
@res[:ProcSet] = :PDF
|
199
194
|
@res.validate
|
@@ -128,4 +128,12 @@ describe HexaPDF::Type::Signature do
|
|
128
128
|
assert_kind_of(OpenSSL::X509::Store, store)
|
129
129
|
assert(kwargs[:allow_self_signed])
|
130
130
|
end
|
131
|
+
|
132
|
+
describe "perform_validation" do
|
133
|
+
it "upgrades the version to 2.0 if the /SubFilter needs it" do
|
134
|
+
refute(@sig.validate(auto_correct: false))
|
135
|
+
assert(@sig.validate(auto_correct: true))
|
136
|
+
assert_equal('2.0', @doc.version)
|
137
|
+
end
|
138
|
+
end
|
131
139
|
end
|
data/test/test_helper.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hexapdf
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.27.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Thomas Leitner
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-11-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: cmdparse
|
@@ -44,6 +44,20 @@ dependencies:
|
|
44
44
|
- - "~>"
|
45
45
|
- !ruby/object:Gem::Version
|
46
46
|
version: '0.3'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: openssl
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 2.2.1
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: 2.2.1
|
47
61
|
- !ruby/object:Gem::Dependency
|
48
62
|
name: kramdown
|
49
63
|
requirement: !ruby/object:Gem::Requirement
|
@@ -699,7 +713,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
699
713
|
requirements:
|
700
714
|
- - ">="
|
701
715
|
- !ruby/object:Gem::Version
|
702
|
-
version: '2.
|
716
|
+
version: '2.6'
|
703
717
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
704
718
|
requirements:
|
705
719
|
- - ">="
|