hexapdf 0.24.2 → 0.25.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.
@@ -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