hexapdf 0.24.2 → 0.25.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -267,6 +267,49 @@ describe HexaPDF::Revisions do
267
267
  assert_equal(2, doc.revisions.count)
268
268
  end
269
269
 
270
+ it "merges a completely empty revision with just a /XRefStm with the previous revision" do
271
+ io = StringIO.new(<<~EOF) # 2 28 3 47
272
+ %PDF-1.7
273
+ 1 0 obj
274
+ 10
275
+ endobj
276
+
277
+ 2 0 obj
278
+ 20
279
+ endobj
280
+
281
+ 3 0 obj
282
+ << /Type /XRef /Size 3 /Index [2 1] /W [1 1 1] /Filter /ASCIIHexDecode /Length 6
283
+ >>stream
284
+ 011C00
285
+ endstream
286
+ endobj
287
+
288
+ xref
289
+ 0 4
290
+ 0000000000 65535 f
291
+ 0000000009 00000 n
292
+ 0000000000 65535 f
293
+ 0000000047 00000 n
294
+ trailer
295
+ << /Size 3 >>
296
+ startxref
297
+ 170
298
+ %%EOF
299
+
300
+ xref
301
+ 0 0
302
+ trailer
303
+ << /Size 3 /Prev 170 /XRefStm 47>>
304
+ startxref
305
+ 302
306
+ %%EOF
307
+ EOF
308
+ doc = HexaPDF::Document.new(io: io)
309
+ assert_equal(1, doc.revisions.count)
310
+ assert_equal(20, doc.object(2).value)
311
+ end
312
+
270
313
  it "uses the reconstructed revision if errors are found when loading from an IO" do
271
314
  io = StringIO.new(<<~EOF)
272
315
  %PDF-1.7
@@ -300,5 +343,9 @@ describe HexaPDF::Revisions do
300
343
  doc = HexaPDF::Document.new(io: io)
301
344
  assert_equal(2, doc.revisions.count)
302
345
  assert_same(doc.revisions.all[0].trailer.value, doc.revisions.all[1].trailer.value)
346
+
347
+ assert_raises(HexaPDF::MalformedPDFError) do
348
+ HexaPDF::Document.new(io: io, config: {'parser.try_xref_reconstruction' => false})
349
+ end
303
350
  end
304
351
  end
@@ -40,7 +40,7 @@ describe HexaPDF::Writer do
40
40
  219
41
41
  %%EOF
42
42
  3 0 obj
43
- <</Producer(HexaPDF version 0.24.2)>>
43
+ <</Producer(HexaPDF version 0.25.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.24.2)>>
75
+ <</Producer(HexaPDF version 0.25.0)>>
76
76
  endobj
77
77
  2 0 obj
78
78
  <</Length 10>>stream
@@ -206,7 +206,7 @@ describe HexaPDF::Writer do
206
206
  <</Type/Page/MediaBox[0 0 595 842]/Parent 2 0 R/Resources<<>>>>
207
207
  endobj
208
208
  5 0 obj
209
- <</Producer(HexaPDF version 0.24.2)>>
209
+ <</Producer(HexaPDF version 0.25.0)>>
210
210
  endobj
211
211
  4 0 obj
212
212
  <</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
@@ -29,6 +29,13 @@ 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])
34
+ outline = @catalog.outline
35
+ assert_equal(:Outlines, outline.type)
36
+ assert_same(outline, @catalog.outline)
37
+ end
38
+
32
39
  describe "acro_form" do
33
40
  it "returns an existing form object" do
34
41
  @catalog[:AcroForm] = :test
@@ -26,39 +26,40 @@ describe HexaPDF::Type::ObjectStream do
26
26
  def (@doc).trailer
27
27
  @trailer ||= {Encrypt: HexaPDF::Object.new({}, oid: 9)}
28
28
  end
29
- @obj = HexaPDF::Type::ObjectStream.new({}, oid: 1, document: @doc)
29
+ @obj = HexaPDF::Type::ObjectStream.new({N: 2, First: 8}, oid: 1, document: @doc,
30
+ stream: "1 0 5 2 5 [1 2]")
30
31
  end
31
32
 
32
33
  it "correctly parses stream data" do
33
- @obj.value = {N: 2, First: 8}
34
- @obj.stream = "1 0 5 2 5 [1 2]"
35
34
  data = @obj.parse_stream
36
35
  assert_equal([5, 1], data.object_by_index(0))
37
36
  assert_equal([[1, 2], 5], data.object_by_index(1))
38
37
  end
39
38
 
40
- it "allows adding and deleting object as well as determining their index" do
39
+ it "allows adding and deleting objects as well as determining their index" do
41
40
  @obj.add_object(5)
42
41
  @obj.add_object(7)
43
42
  @obj.add_object(9)
44
43
  @obj.add_object(5)
45
- assert_equal(0, @obj.object_index(5))
46
- assert_equal(1, @obj.object_index(7))
47
- assert_equal(2, @obj.object_index(9))
44
+ assert_equal(2, @obj.object_index(5))
45
+ assert_equal(3, @obj.object_index(7))
46
+ assert_equal(4, @obj.object_index(9))
48
47
 
49
48
  @obj.delete_object(5)
50
49
  @obj.delete_object(5)
51
- assert_equal(0, @obj.object_index(9))
52
- assert_equal(1, @obj.object_index(7))
50
+ assert_equal(2, @obj.object_index(9))
51
+ assert_equal(3, @obj.object_index(7))
53
52
  assert_nil(@obj.object_index(5))
54
53
 
55
54
  @obj.delete_object(7)
56
55
  @obj.delete_object(9)
57
- assert_nil(@obj.object_index(5))
56
+ assert_nil(@obj.object_index(7))
58
57
  end
59
58
 
60
59
  describe "write objects to stream" do
61
60
  before do
61
+ @obj.delete_object(HexaPDF::Reference.new(1))
62
+ @obj.delete_object(HexaPDF::Reference.new(5))
62
63
  @revision = Object.new
63
64
  def @revision.object(obj); obj; end
64
65
  end
@@ -0,0 +1,69 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'test_helper'
4
+ require 'hexapdf/document'
5
+ require 'hexapdf/type/outline'
6
+
7
+ describe HexaPDF::Type::Outline do
8
+ before do
9
+ @doc = HexaPDF::Document.new
10
+ @outline = @doc.add({}, type: :Outlines)
11
+ end
12
+
13
+ it "delegates add_item to the outline item wrapper" do
14
+ item = @outline.add_item("test", position: :first, text_color: "blue", flags: [:italic])
15
+ assert_equal("test", item.title)
16
+ assert_equal([0, 0, 1], item.text_color.components)
17
+ assert_equal([:italic], item.flags)
18
+ end
19
+
20
+ it "recursively iterates over all items by delegating to the outline item wrapper" do
21
+ @outline.add_item("Item1") do |item1|
22
+ item1.add_item("Item2")
23
+ item1.add_item("Item3") do |item3|
24
+ item3.add_item("Item4")
25
+ end
26
+ item1.add_item("Item5")
27
+ end
28
+ assert_equal(%w[Item1 Item2 Item3 Item4 Item5], @outline.each_item.map(&:title))
29
+ end
30
+
31
+ describe "perform_validation" do
32
+ before do
33
+ 5.times { @outline.add_item("Test1") }
34
+ end
35
+
36
+ it "fixes a missing /First entry" do
37
+ @outline.delete(:First)
38
+ called = false
39
+ @outline.validate do |msg, correctable, _|
40
+ called = true
41
+ assert_match(/missing an endpoint reference/, msg)
42
+ assert(correctable)
43
+ end
44
+ assert(called)
45
+ end
46
+
47
+ it "fixes a missing /Last entry" do
48
+ @outline.delete(:Last)
49
+ called = false
50
+ @outline.validate do |msg, correctable, _|
51
+ called = true
52
+ assert_match(/missing an endpoint reference/, msg)
53
+ assert(correctable)
54
+ end
55
+ assert(called)
56
+ end
57
+
58
+ it "deletes the /Count entry if no /First and /Last entries exist" do
59
+ @outline.delete(:Last)
60
+ @outline.delete(:First)
61
+ assert_equal(5, @outline[:Count])
62
+ @outline.validate do |msg, correctable, _|
63
+ assert_match(/key \/Count set but no items exist/, msg)
64
+ assert(correctable)
65
+ end
66
+ refute(@outline.key?(:Count))
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,292 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'test_helper'
4
+ require 'hexapdf/document'
5
+ require 'hexapdf/type/outline_item'
6
+
7
+ describe HexaPDF::Type::OutlineItem do
8
+ before do
9
+ @doc = HexaPDF::Document.new
10
+ @item = @doc.add({Title: "root", Count: 0}, type: :XXOutlineItem)
11
+ end
12
+
13
+ describe "title" do
14
+ it "returns the set title" do
15
+ @item[:Title] = 'Test'
16
+ assert_equal('Test', @item.title)
17
+ end
18
+
19
+ it "sets the title to the given value" do
20
+ @item.title('Test')
21
+ assert_equal('Test', @item[:Title])
22
+ end
23
+ end
24
+
25
+ describe "text_color" do
26
+ it "returns the default color if none is set" do
27
+ assert_equal([0, 0, 0], @item.text_color.components)
28
+ end
29
+
30
+ it "returns the set color" do
31
+ @item[:C] = [0, 0.5, 1]
32
+ assert_equal([0, 0.5, 1], @item.text_color.components)
33
+ end
34
+
35
+ it "sets the text color to the given value" do
36
+ @item.text_color([51, 51, 255])
37
+ assert_equal([0.2, 0.2, 1], @item[:C])
38
+ end
39
+
40
+ it "fails if a color in another color space is set" do
41
+ assert_raises(ArgumentError) { @item.text_color(5) }
42
+ end
43
+ end
44
+
45
+ describe "destination" do
46
+ it "returns the set destination" do
47
+ @item[:Dest] = [5, :Fit]
48
+ assert_equal([5, :Fit], @item.destination)
49
+ end
50
+
51
+ it "sets the destination to the given value" do
52
+ @item.destination(@doc.pages.add)
53
+ assert_equal([@doc.pages[0], :Fit], @item[:Dest])
54
+ end
55
+
56
+ it "deletes an existing action entry when setting a value" do
57
+ @item[:A] = {S: :GoTo}
58
+ @item.destination(@doc.pages.add)
59
+ refute(@item.key?(:A))
60
+ end
61
+ end
62
+
63
+ describe "action" do
64
+ it "returns the set action" do
65
+ @item[:A] = {S: :GoTo}
66
+ assert_equal({S: :GoTo}, @item.action.value)
67
+ end
68
+
69
+ it "sets the action to the given value" do
70
+ @item.action({S: :GoTo})
71
+ assert_equal({S: :GoTo}, @item[:A].value)
72
+ end
73
+
74
+ it "deletes an existing destination entry when setting a value" do
75
+ @item[:Dest] = [1, :Fit]
76
+ @item.action({S: :GoTo})
77
+ refute(@item.key?(:Dest))
78
+ end
79
+ end
80
+
81
+ describe "add" do
82
+ it "returns the created item" do
83
+ new_item = @item.add_item("Test")
84
+ assert_equal("Test", new_item.title)
85
+ assert_equal(0, new_item[:Count])
86
+ assert_same(@item, new_item[:Parent])
87
+ assert(new_item.indirect?)
88
+ end
89
+
90
+ it "sets the item's text color" do
91
+ new_item = @item.add_item("Test", text_color: "red")
92
+ assert_equal([1, 0, 0], new_item.text_color.components)
93
+ end
94
+
95
+ it "sets the item's flags" do
96
+ new_item = @item.add_item("Test", flags: [:bold, :italic])
97
+ assert_equal([:italic, :bold], new_item.flags)
98
+ end
99
+
100
+ it "doesn't set the item's /Count when it should not be open" do
101
+ new_item = @item.add_item("Test", open: false)
102
+ refute(new_item.key?(:Count))
103
+ end
104
+
105
+ it "sets the item's destination if given" do
106
+ new_item = @item.add_item("Test", destination: @doc.pages.add)
107
+ assert_equal([@doc.pages[0], :Fit], new_item.destination)
108
+ end
109
+
110
+ it "sets the item's action if given" do
111
+ new_item = @item.add_item("Test", action: {S: :GoTo, D: [1, :Fit]})
112
+ assert_equal({S: :GoTo, D: [1, :Fit]}, new_item.action.value)
113
+ end
114
+
115
+ it "yields the item" do
116
+ yielded_item = nil
117
+ new_item = @item.add_item("Test") {|i| yielded_item = i }
118
+ assert_same(new_item, yielded_item)
119
+ end
120
+
121
+ describe "position" do
122
+ it "works for an empty item" do
123
+ new_item = @item.add_item("Test")
124
+ assert_same(new_item, @item[:First])
125
+ assert_same(new_item, @item[:Last])
126
+ assert_nil(new_item[:Next])
127
+ assert_nil(new_item[:Prev])
128
+ end
129
+
130
+ it "inserts an item at the last position with at least one existing sub-item" do
131
+ first_item = @item.add_item("Test")
132
+ second_item = @item.add_item("Test", position: :last)
133
+ assert_same(first_item, @item[:First])
134
+ assert_same(second_item, @item[:Last])
135
+ assert_same(second_item, first_item[:Next])
136
+ assert_same(first_item, second_item[:Prev])
137
+ end
138
+
139
+ it "inserts an item at the first position with at least one existing sub-item" do
140
+ second_item = @item.add_item("Test")
141
+ first_item = @item.add_item("Test", position: :first)
142
+ assert_same(first_item, @item[:First])
143
+ assert_same(second_item, @item[:Last])
144
+ assert_same(second_item, first_item[:Next])
145
+ assert_same(first_item, second_item[:Prev])
146
+ end
147
+
148
+ it "inserts an item at an arbitrary positive index" do
149
+ 5.times {|i| @item.add_item("Test#{i}") }
150
+ @item.add_item("Test", position: 3)
151
+ item = @item[:First]
152
+ %w[Test0 Test1 Test2 Test Test3 Test4].each do |title|
153
+ assert_equal(title, item.title)
154
+ item = item[:Next]
155
+ end
156
+ end
157
+
158
+ it "inserts an item at an arbitrary negative index" do
159
+ 5.times {|i| @item.add_item("Test#{i}") }
160
+ @item.add_item("Test", position: -3)
161
+ item = @item[:First]
162
+ %w[Test0 Test1 Test2 Test Test3 Test4].each do |title|
163
+ assert_equal(title, item.title)
164
+ item = item[:Next]
165
+ end
166
+ end
167
+
168
+ it "raises an out of bounds error for invalid integer values" do
169
+ 5.times {|i| @item.add_item("Test#{i}") }
170
+ assert_raises(ArgumentError) { @item.add_item("Test", position: 10) }
171
+ assert_raises(ArgumentError) { @item.add_item("Test", position: -10) }
172
+ end
173
+
174
+ it "raises an error for an invalid value" do
175
+ assert_raises(ArgumentError) { @item.add_item("Test", position: :luck) }
176
+ end
177
+ end
178
+
179
+ it "calculcates the /Count values correctly" do
180
+ [
181
+ [[true, true], [6, 4, 0, 1, 0, 0, 0]],
182
+ [[true, false], [5, 3, 0, -1, 0, 0, 0]],
183
+ [[false, true], [2, -4, 0, 1, 0, 0, 0]],
184
+ [[false, false], [2, -3, 0, -1, 0, 0, 0]],
185
+ ].each do |(states, result)|
186
+ # reset list
187
+ @item[:First] = @item[:Last] = nil
188
+ @item[:Count] = 0
189
+
190
+ items = [@item]
191
+ @item.add_item("Document", open: states[0]) do |idoc|
192
+ items << idoc
193
+ items << idoc.add_item("Section 1", open: false)
194
+ idoc.add_item("Section 2", open: states[1]) do |isec|
195
+ items << isec
196
+ items << isec.add_item("Subsection 1")
197
+ end
198
+ items << idoc.add_item("Section 3")
199
+ end
200
+ items << @item.add_item("Summary")
201
+ items.each_with_index {|item, index| assert_equal(result.shift, item[:Count] || 0, "item#{index}") }
202
+ end
203
+ end
204
+ end
205
+
206
+ it "recursively iterates over all descendant items" do
207
+ @item.add_item("Item1") do |item1|
208
+ item1.add_item("Item2")
209
+ item1.add_item("Item3") do |item3|
210
+ item3.add_item("Item4")
211
+ end
212
+ item1.add_item("Item5")
213
+ end
214
+ assert_equal(%w[Item1 Item2 Item3 Item4 Item5], @item.each_item.map(&:title))
215
+ end
216
+
217
+ describe "perform_validation" do
218
+ before do
219
+ 5.times { @item.add_item("Test1") }
220
+ @item[:Parent] = @doc.add({})
221
+ end
222
+
223
+ it "fixes a missing /First entry" do
224
+ @item.delete(:First)
225
+ called = false
226
+ @item.validate do |msg, correctable, _|
227
+ called = true
228
+ assert_match(/missing an endpoint reference/, msg)
229
+ assert(correctable)
230
+ end
231
+ assert(called)
232
+ end
233
+
234
+ it "fixes a missing /Last entry" do
235
+ @item.delete(:Last)
236
+ called = false
237
+ @item.validate do |msg, correctable, _|
238
+ called = true
239
+ assert_match(/missing an endpoint reference/, msg)
240
+ assert(correctable)
241
+ end
242
+ assert(called)
243
+ end
244
+
245
+ it "deletes the /Count entry if no /First and /Last entries exist" do
246
+ @item.delete(:Last)
247
+ @item.delete(:First)
248
+ assert_equal(5, @item[:Count])
249
+ @item.validate do |msg, correctable, _|
250
+ assert_match(/\/Count set but no descendants/, msg)
251
+ assert(correctable)
252
+ end
253
+ refute(@item.key?(:Count))
254
+ end
255
+
256
+ it "fails validation if the previous item's /Next points somewhere else" do
257
+ item = @item[:First][:Next]
258
+ item[:Prev][:Next] = item[:Next]
259
+ item.validate do |msg, correctable, _|
260
+ assert_match(/\/Prev points to item whose \/Next points somewhere else/, msg)
261
+ refute(correctable)
262
+ end
263
+ end
264
+
265
+ it "corrects the previous item's missing /Next entry" do
266
+ item = @item[:First][:Next]
267
+ item[:Prev].delete(:Next)
268
+ item.validate do |msg, correctable, _|
269
+ assert_match(/\/Prev points to item without \/Next/, msg)
270
+ assert(correctable)
271
+ end
272
+ end
273
+
274
+ it "fails validation if the next item's /Prev points somewhere else" do
275
+ item = @item[:First][:Next]
276
+ item[:Next][:Prev] = item[:Prev]
277
+ item.validate do |msg, correctable, _|
278
+ assert_match(/\/Next points to item whose \/Prev points somewhere else/, msg)
279
+ refute(correctable)
280
+ end
281
+ end
282
+
283
+ it "corrects the next item's missing /Prev entry" do
284
+ item = @item[:First][:Next]
285
+ item[:Next].delete(:Prev)
286
+ item.validate do |msg, correctable, _|
287
+ assert_match(/\/Next points to item without \/Prev/, msg)
288
+ assert(correctable)
289
+ end
290
+ end
291
+ end
292
+ end
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.24.2
4
+ version: 0.25.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-08-30 00:00:00.000000000 Z
11
+ date: 2022-10-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: cmdparse
@@ -233,6 +233,7 @@ files:
233
233
  - examples/019-acro_form.rb
234
234
  - examples/020-column_box.rb
235
235
  - examples/021-list_box.rb
236
+ - examples/022-outline.rb
236
237
  - examples/emoji-smile.png
237
238
  - examples/emoji-wink.png
238
239
  - examples/machupicchu.jpg
@@ -421,6 +422,8 @@ files:
421
422
  - lib/hexapdf/type/info.rb
422
423
  - lib/hexapdf/type/names.rb
423
424
  - lib/hexapdf/type/object_stream.rb
425
+ - lib/hexapdf/type/outline.rb
426
+ - lib/hexapdf/type/outline_item.rb
424
427
  - lib/hexapdf/type/page.rb
425
428
  - lib/hexapdf/type/page_tree_node.rb
426
429
  - lib/hexapdf/type/resources.rb
@@ -664,6 +667,8 @@ files:
664
667
  - test/hexapdf/type/test_info.rb
665
668
  - test/hexapdf/type/test_names.rb
666
669
  - test/hexapdf/type/test_object_stream.rb
670
+ - test/hexapdf/type/test_outline.rb
671
+ - test/hexapdf/type/test_outline_item.rb
667
672
  - test/hexapdf/type/test_page.rb
668
673
  - test/hexapdf/type/test_page_tree_node.rb
669
674
  - test/hexapdf/type/test_resources.rb
@@ -698,7 +703,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
698
703
  - !ruby/object:Gem::Version
699
704
  version: '0'
700
705
  requirements: []
701
- rubygems_version: 3.2.32
706
+ rubygems_version: 3.3.3
702
707
  signing_key:
703
708
  specification_version: 4
704
709
  summary: HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby