hexapdf 0.22.0 → 0.23.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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +50 -0
  3. data/lib/hexapdf/cli/form.rb +26 -3
  4. data/lib/hexapdf/cli/inspect.rb +12 -3
  5. data/lib/hexapdf/cli/modify.rb +23 -3
  6. data/lib/hexapdf/composer.rb +24 -2
  7. data/lib/hexapdf/document/destinations.rb +396 -0
  8. data/lib/hexapdf/document.rb +38 -89
  9. data/lib/hexapdf/layout/frame.rb +8 -9
  10. data/lib/hexapdf/layout/style.rb +280 -7
  11. data/lib/hexapdf/layout/text_box.rb +10 -2
  12. data/lib/hexapdf/layout/text_layouter.rb +6 -1
  13. data/lib/hexapdf/revision.rb +8 -1
  14. data/lib/hexapdf/revisions.rb +151 -50
  15. data/lib/hexapdf/task/optimize.rb +21 -11
  16. data/lib/hexapdf/type/acro_form/text_field.rb +8 -0
  17. data/lib/hexapdf/type/catalog.rb +9 -1
  18. data/lib/hexapdf/type/names.rb +13 -0
  19. data/lib/hexapdf/type/xref_stream.rb +2 -1
  20. data/lib/hexapdf/utils/sorted_tree_node.rb +3 -1
  21. data/lib/hexapdf/version.rb +1 -1
  22. data/lib/hexapdf/writer.rb +15 -2
  23. data/test/hexapdf/document/test_destinations.rb +338 -0
  24. data/test/hexapdf/encryption/test_security_handler.rb +2 -2
  25. data/test/hexapdf/layout/test_frame.rb +15 -1
  26. data/test/hexapdf/layout/test_text_box.rb +16 -0
  27. data/test/hexapdf/layout/test_text_layouter.rb +7 -0
  28. data/test/hexapdf/task/test_optimize.rb +17 -4
  29. data/test/hexapdf/test_composer.rb +24 -1
  30. data/test/hexapdf/test_document.rb +30 -133
  31. data/test/hexapdf/test_parser.rb +1 -1
  32. data/test/hexapdf/test_revision.rb +14 -0
  33. data/test/hexapdf/test_revisions.rb +137 -29
  34. data/test/hexapdf/test_writer.rb +43 -14
  35. data/test/hexapdf/type/acro_form/test_text_field.rb +17 -0
  36. data/test/hexapdf/type/test_catalog.rb +8 -0
  37. data/test/hexapdf/type/test_names.rb +20 -0
  38. data/test/hexapdf/type/test_xref_stream.rb +2 -1
  39. data/test/hexapdf/utils/test_sorted_tree_node.rb +11 -1
  40. metadata +5 -2
@@ -91,43 +91,6 @@ describe HexaPDF::Document do
91
91
  end
92
92
  end
93
93
 
94
- describe "object" do
95
- it "accepts a Reference object as argument" do
96
- assert_equal(10, @io_doc.object(HexaPDF::Reference.new(1, 0)).value)
97
- end
98
-
99
- it "accepts an object number as arguments" do
100
- assert_equal(10, @io_doc.object(1).value)
101
- end
102
-
103
- it "returns added objects" do
104
- obj = @io_doc.add(@io_doc.wrap({Type: :Test}, oid: 100))
105
- assert_equal(obj, @io_doc.object(100))
106
- end
107
-
108
- it "returns nil for unknown object references" do
109
- assert_nil(@io_doc.object(100))
110
- end
111
-
112
- it "returns only the newest version of an object" do
113
- assert_equal(200, @io_doc.object(2).value)
114
- assert_equal(200, @io_doc.object(HexaPDF::Reference.new(2, 0)).value)
115
- assert_nil(@io_doc.object(3).value)
116
- assert_nil(@io_doc.object(HexaPDF::Reference.new(3, 1)).value)
117
- assert_equal(30, @io_doc.object(HexaPDF::Reference.new(3, 0)).value)
118
- end
119
- end
120
-
121
- describe "object?" do
122
- it "works with a Reference object as argument" do
123
- assert(@io_doc.object?(HexaPDF::Reference.new(1, 0)))
124
- end
125
-
126
- it "works with an object number as arguments" do
127
- assert(@io_doc.object?(1))
128
- end
129
- end
130
-
131
94
  describe "deref" do
132
95
  it "returns a dereferenced object when given a Reference object" do
133
96
  assert_equal(@io_doc.object(1), @io_doc.deref(HexaPDF::Reference.new(1, 0)))
@@ -139,13 +102,6 @@ describe HexaPDF::Document do
139
102
  end
140
103
 
141
104
  describe "add" do
142
- it "automatically assigns free object numbers" do
143
- assert_equal(1, @doc.add(5).oid)
144
- assert_equal(2, @doc.add(5).oid)
145
- @doc.revisions.add
146
- assert_equal(3, @doc.add(5).oid)
147
- end
148
-
149
105
  it "assigns the object's document" do
150
106
  obj = @doc.add(5)
151
107
  assert_equal(@doc, obj.document)
@@ -166,82 +122,38 @@ describe HexaPDF::Document do
166
122
  assert_equal(5, obj.value)
167
123
  end
168
124
 
169
- it "returns the given object if it is already stored in the document" do
170
- obj = @doc.add(5)
171
- assert_same(obj, @doc.add(obj))
172
- end
173
-
174
- it "allows specifying a revision to which the object should be added" do
175
- @doc.revisions.add
176
- @doc.revisions.add
177
-
178
- @doc.add(@doc.wrap(5, oid: 1), revision: 0)
179
- assert_equal(5, @doc.object(1).value)
180
-
181
- @doc.add(@doc.wrap(10, oid: 1), revision: 2)
182
- assert_equal(10, @doc.object(1).value)
183
-
184
- @doc.add(@doc.wrap(7.5, oid: 1), revision: 1)
185
- assert_equal(10, @doc.object(1).value)
186
- end
187
-
188
- it "fails if the specified revision index is invalid" do
189
- assert_raises(ArgumentError) { @doc.add(5, revision: 5) }
190
- end
191
-
192
125
  it "fails if the object to be added is associated with another document" do
193
126
  doc = HexaPDF::Document.new
194
127
  obj = doc.add(5)
195
128
  assert_raises(HexaPDF::Error) { @doc.add(obj) }
196
129
  end
197
-
198
- it "fails if the object number is already associated with another object" do
199
- obj = @doc.add(5)
200
- assert_raises(HexaPDF::Error) { @doc.add(@doc.wrap(5, oid: obj.oid, gen: 1)) }
201
- end
202
130
  end
203
131
 
204
- describe "delete" do
205
- it "works with a Reference object as argument" do
206
- obj = @doc.add(5)
207
- @doc.delete(obj, mark_as_free: false)
208
- refute(@doc.object?(obj))
209
- end
210
-
211
- it "works with an object number as arguments" do
212
- @doc.add(5)
213
- @doc.delete(1, mark_as_free: false)
214
- refute(@doc.object?(1))
215
- end
216
-
217
- describe "with an object in multiple revisions" do
218
- before do
219
- @ref = HexaPDF::Reference.new(2, 3)
220
- obj = @doc.wrap(5, oid: @ref.oid, gen: @ref.gen)
221
- @doc.revisions.add
222
- @doc.add(obj, revision: 0)
223
- @doc.add(obj, revision: 1)
224
- end
225
-
226
- it "deletes an object for all revisions when revision = :all" do
227
- @doc.delete(@ref, revision: :all, mark_as_free: false)
228
- refute(@doc.object?(@ref))
229
- end
230
-
231
- it "deletes an object only in the current revision when revision = :current" do
232
- @doc.delete(@ref, revision: :current, mark_as_free: false)
233
- assert(@doc.object?(@ref))
234
- end
132
+ it "defers to @revisions for retrieving an object" do
133
+ revs = Minitest::Mock.new
134
+ revs.expect(:object, :retval, [:ref])
135
+ doc = HexaPDF::Document.new
136
+ doc.instance_variable_set(:@revisions, revs)
137
+ doc.object(:ref)
138
+ revs.verify
139
+ end
235
140
 
236
- it "marks the object as PDF null object when using mark_as_free=true" do
237
- @doc.delete(@ref, revision: :current)
238
- assert(@doc.object(@ref).null?)
239
- end
240
- end
141
+ it "defers to @revisions for checking for the existence of an object" do
142
+ revs = Minitest::Mock.new
143
+ revs.expect(:object?, :retval, [:ref])
144
+ doc = HexaPDF::Document.new
145
+ doc.instance_variable_set(:@revisions, revs)
146
+ doc.object?(:ref)
147
+ revs.verify
148
+ end
241
149
 
242
- it "fails if the revision argument is invalid" do
243
- assert_raises(ArgumentError) { @doc.delete(1, revision: :invalid) }
244
- end
150
+ it "defers to @revisions for deleting an object" do
151
+ revs = Minitest::Mock.new
152
+ revs.expect(:delete_object, :retval, [:ref])
153
+ doc = HexaPDF::Document.new
154
+ doc.instance_variable_set(:@revisions, revs)
155
+ doc.delete(:ref)
156
+ revs.verify
245
157
  end
246
158
 
247
159
  describe "import" do
@@ -388,28 +300,13 @@ describe HexaPDF::Document do
388
300
  end
389
301
  end
390
302
 
391
- describe "each" do
392
- it "iterates over the current objects" do
393
- assert_equal([10, 200, nil], @io_doc.each(only_current: true).sort.map(&:value))
394
- end
395
-
396
- it "iterates over all objects" do
397
- assert_equal([10, 200, 20, 30, nil], @io_doc.each(only_current: false).sort.map(&:value))
398
- end
399
-
400
- it "iterates over all loaded objects" do
401
- assert_equal(200, @io_doc.object(2).value)
402
- assert_equal([200], @io_doc.each(only_loaded: true).sort.map(&:value))
403
- end
404
-
405
- it "yields the revision as second argument if the block accepts exactly two arguments" do
406
- objs = [[10, 20, 30], [200, nil]]
407
- data = @io_doc.revisions.map.with_index {|rev, i| objs[i].map {|o| [o, rev] } }.reverse.flatten
408
- @io_doc.each(only_current: false) do |obj, rev|
409
- assert(data.shift == obj.value)
410
- assert_equal(data.shift, rev)
411
- end
412
- end
303
+ it "defers to @revisions for iterating over all objects" do
304
+ revs = Minitest::Mock.new
305
+ revs.expect(:each_object, :retval, [{only_current: true, only_loaded: true}])
306
+ doc = HexaPDF::Document.new
307
+ doc.instance_variable_set(:@revisions, revs)
308
+ doc.each(only_current: true, only_loaded: true)
309
+ revs.verify
413
310
  end
414
311
 
415
312
  describe "encryption" do
@@ -548,7 +548,7 @@ describe HexaPDF::Parser do
548
548
 
549
549
  it "works for a cross-reference stream" do
550
550
  xref_section, trailer = @parser.load_revision(212)
551
- assert_equal({Size: 2}, trailer)
551
+ assert_equal({Size: 2, Type: :XRef}, trailer)
552
552
  assert(xref_section[1].in_use?)
553
553
  assert(@parser.contains_xref_streams?)
554
554
  end
@@ -215,4 +215,18 @@ describe HexaPDF::Revision do
215
215
  assert_equal([], @rev.each_modified_object.to_a)
216
216
  end
217
217
  end
218
+
219
+ describe "reset_objects" do
220
+ it "deletes loaded objects" do
221
+ @rev.object(2)
222
+ @rev.reset_objects
223
+ assert(@rev.instance_variable_get(:@objects).oids.empty?)
224
+ end
225
+
226
+ it "deletes added objects" do
227
+ @rev.add(@obj)
228
+ @rev.reset_objects
229
+ assert(@rev.instance_variable_get(:@objects).oids.empty?)
230
+ end
231
+ end
218
232
  end
@@ -70,61 +70,169 @@ describe HexaPDF::Revisions do
70
70
  @revisions = @doc.revisions
71
71
  end
72
72
 
73
- describe "add" do
74
- it "adds an empty revision as the current revision" do
75
- rev = @revisions.add
76
- assert_equal({Size: 4}, rev.trailer.value)
77
- assert_equal(rev, @revisions.current)
73
+ describe "initialize" do
74
+ it "automatically loads all revisions from the underlying IO object" do
75
+ assert_kind_of(HexaPDF::Parser, @revisions.parser)
76
+ assert_equal(20, @revisions.all[0].object(2).value)
77
+ assert_equal(200, @revisions.all[1].object(2).value)
78
+ assert_equal(400, @revisions.all[2].object(2).value)
79
+ end
80
+
81
+ it "creates an empty revision when not using initial revisions" do
82
+ revisions = HexaPDF::Revisions.new(@doc)
83
+ assert_equal(1, revisions.all.count)
78
84
  end
79
85
  end
80
86
 
81
- describe "delete_revision" do
82
- it "allows deleting a revision by index" do
83
- rev = @revisions.revision(0)
84
- @revisions.delete(0)
85
- refute(@revisions.any? {|r| r == rev })
87
+ it "returns the next free oid" do
88
+ assert_equal(4, @revisions.next_oid)
89
+ end
90
+
91
+ describe "object" do
92
+ it "accepts a Reference object as argument" do
93
+ assert_equal(400, @revisions.object(HexaPDF::Reference.new(2, 0)).value)
94
+ end
95
+
96
+ it "accepts an object number as arguments" do
97
+ assert_equal(400, @revisions.object(2).value)
86
98
  end
87
99
 
88
- it "allows deleting a revision by specifying a revision" do
89
- rev = @revisions.revision(0)
90
- @revisions.delete(rev)
91
- refute(@revisions.any? {|r| r == rev })
100
+ it "returns nil for unknown object references" do
101
+ assert_nil(@revisions.object(100))
92
102
  end
93
103
 
94
- it "fails when trying to delete the only existing revision" do
95
- assert_raises(HexaPDF::Error) { @revisions.delete(0) while @revisions.current }
104
+ it "returns a null object for freed objects" do
105
+ @revisions.delete_object(2)
106
+ assert(@revisions.object(2).null?)
107
+ end
108
+ end
109
+
110
+ describe "object?" do
111
+ it "works with a Reference object as argument" do
112
+ assert(@revisions.object?(HexaPDF::Reference.new(2, 0)))
113
+ end
114
+
115
+ it "works with an object number as arguments" do
116
+ assert(@revisions.object?(2))
117
+ end
118
+
119
+ it "returns false when no object is found" do
120
+ refute(@revisions.object?(20))
121
+ end
122
+
123
+ it "returns true for freed objects" do
124
+ @revisions.delete_object(2)
125
+ assert(@revisions.object?(2))
126
+ end
127
+ end
128
+
129
+ describe "add_object" do
130
+ before do
131
+ @obj = HexaPDF::Object.new(5)
132
+ end
133
+
134
+ it "adds the object to the current revision" do
135
+ @revisions.add_object(@obj)
136
+ assert_same(@obj, @revisions.current.object(@obj))
137
+ end
138
+
139
+ it "returns the added object" do
140
+ obj = @revisions.add_object(@obj)
141
+ assert_same(@obj, obj)
142
+ end
143
+
144
+ it "returns the given object if it is already stored in the document" do
145
+ obj = @revisions.add_object(@obj)
146
+ assert_same(obj, @revisions.add_object(obj))
147
+ end
148
+
149
+ it "fails if the object number is already associated with another object" do
150
+ @revisions.add_object(@obj)
151
+ assert_raises(HexaPDF::Error) { @revisions.add_object(@doc.wrap(5, oid: @obj.oid)) }
152
+ end
153
+
154
+ it "automatically assign an object number for direct objects" do
155
+ assert_equal(4, @revisions.add_object(@obj).oid)
156
+ end
157
+ end
158
+
159
+ describe "delete_object" do
160
+ it "works with a Reference object as argument" do
161
+ @revisions.delete_object(@doc.object(2))
162
+ assert(@revisions.object(2).null?)
163
+ end
164
+
165
+ it "works with an object number as arguments" do
166
+ @revisions.delete_object(2)
167
+ assert(@revisions.object(2).null?)
168
+ end
169
+
170
+ it "deletes an object only in the most recent revision" do
171
+ @revisions.delete_object(2)
172
+ assert_equal(20, @revisions.all[0].object(2).value)
173
+ assert_equal(200, @revisions.all[1].object(2).value)
174
+ assert(@revisions.all[2].object(2).null?)
175
+ end
176
+ end
177
+
178
+ describe "each_object" do
179
+ before do
180
+ @obj3 = @revisions.object(3).value
181
+ end
182
+
183
+ it "iterates over the current objects" do
184
+ assert_equal([10, 400, @obj3], @revisions.each_object(only_current: true).sort.map(&:value))
185
+ end
186
+
187
+ it "iterates over all objects" do
188
+ assert_equal([10, 400, 200, 20, @obj3, @obj3],
189
+ @revisions.each_object(only_current: false).sort.map(&:value))
190
+ end
191
+
192
+ it "iterates over all loaded objects" do
193
+ assert_equal([@obj3], @revisions.each_object(only_loaded: true).map(&:value))
194
+ assert_equal(400, @revisions.object(2).value)
195
+ assert_equal([400, @obj3], @revisions.each_object(only_loaded: true).sort.map(&:value))
196
+ end
197
+
198
+ it "yields the revision as second argument if the block accepts exactly two arguments" do
199
+ data = [@obj3, @revisions.all[-1], 400, @revisions.all[-1], 10, @revisions.all[0]]
200
+ @revisions.each_object do |obj, rev|
201
+ assert_equal(data.shift, obj.value)
202
+ assert_equal(data.shift, rev)
203
+ end
204
+ assert(data.empty?)
205
+ end
206
+ end
207
+
208
+ describe "add" do
209
+ it "adds an empty revision as the current revision" do
210
+ rev = @revisions.add
211
+ assert_equal({Size: 4}, rev.trailer.value)
212
+ assert_equal(rev, @revisions.current)
96
213
  end
97
214
  end
98
215
 
99
216
  describe "merge" do
100
217
  it "does nothing when only one revision is specified" do
101
218
  @revisions.merge(1..1)
102
- assert_equal(3, @revisions.each.to_a.size)
219
+ assert_equal(3, @revisions.all.size)
103
220
  end
104
221
 
105
222
  it "merges the higher into the the lower revision" do
106
223
  @revisions.merge
107
- assert_equal(1, @revisions.each.to_a.size)
224
+ assert_equal(1, @revisions.all.size)
108
225
  assert_equal([10, 400, @doc.object(3).value], @revisions.current.each.to_a.sort.map(&:value))
109
226
  end
110
227
 
111
228
  it "handles objects correctly that are in multiple revisions" do
112
- @revisions.current.add(@revisions[0].object(1))
229
+ @revisions.current.add(@revisions.all[0].object(1))
113
230
  @revisions.merge
114
231
  assert_equal(1, @revisions.each.to_a.size)
115
232
  assert_equal([10, 400, @doc.object(3).value], @revisions.current.each.to_a.sort.map(&:value))
116
233
  end
117
234
  end
118
235
 
119
- describe "initialize" do
120
- it "automatically loads all revisions from the underlying IO object" do
121
- assert_kind_of(HexaPDF::Parser, @revisions.parser)
122
- assert_equal(20, @revisions.revision(0).object(2).value)
123
- assert_equal(300, @revisions[1].object(2).value)
124
- assert_equal(400, @revisions[2].object(2).value)
125
- end
126
- end
127
-
128
236
  it "handles invalid PDFs that have a loop via the xref /Prev or /XRefStm entries" do
129
237
  io = StringIO.new(<<~EOF)
130
238
  %PDF-1.7
@@ -191,6 +299,6 @@ describe HexaPDF::Revisions do
191
299
  EOF
192
300
  doc = HexaPDF::Document.new(io: io)
193
301
  assert_equal(2, doc.revisions.count)
194
- assert_same(doc.revisions[0].trailer.value, doc.revisions[1].trailer.value)
302
+ assert_same(doc.revisions.all[0].trailer.value, doc.revisions.all[1].trailer.value)
195
303
  end
196
304
  end
@@ -40,13 +40,13 @@ describe HexaPDF::Writer do
40
40
  219
41
41
  %%EOF
42
42
  3 0 obj
43
- <</Producer(HexaPDF version 0.22.0)>>
43
+ <</Producer(HexaPDF version 0.23.0)>>
44
44
  endobj
45
45
  xref
46
46
  3 1
47
47
  0000000296 00000 n
48
48
  trailer
49
- <</Prev 219/Size 4/Root<</Type/Catalog>>/Info 3 0 R>>
49
+ <</Size 4/Root<</Type/Catalog>>/Info 3 0 R/Prev 219>>
50
50
  startxref
51
51
  349
52
52
  %%EOF
@@ -64,7 +64,7 @@ describe HexaPDF::Writer do
64
64
  20
65
65
  endobj
66
66
  3 0 obj
67
- <</Size 6/Type/XRef/W[1 1 2]/Index[0 4 5 1]/Filter/FlateDecode/DecodeParms<</Columns 4/Predictor 12>>/Length 31>>stream
67
+ <</Type/XRef/Size 6/W[1 1 2]/Index[0 4 5 1]/Filter/FlateDecode/DecodeParms<</Columns 4/Predictor 12>>/Length 31>>stream
68
68
  x\xDAcb`\xF8\xFF\x9F\x89\x89\x95\x91\x91\xE9\x7F\x19\x03\x03\x13\x83\x10\x88he`\x00\x00B4\x04\x1E
69
69
  endstream
70
70
  endobj
@@ -72,7 +72,7 @@ describe HexaPDF::Writer do
72
72
  141
73
73
  %%EOF
74
74
  6 0 obj
75
- <</Producer(HexaPDF version 0.22.0)>>
75
+ <</Producer(HexaPDF version 0.23.0)>>
76
76
  endobj
77
77
  2 0 obj
78
78
  <</Length 10>>stream
@@ -80,7 +80,7 @@ describe HexaPDF::Writer do
80
80
  endstream
81
81
  endobj
82
82
  4 0 obj
83
- <</Size 7/Prev 141/Root<</Type/Catalog>>/Info 6 0 R/Type/XRef/W[1 2 2]/Index[2 1 4 1 6 1]/Filter/FlateDecode/DecodeParms<</Columns 5/Predictor 12>>/Length 22>>stream
83
+ <</Type/XRef/Size 7/Root<</Type/Catalog>>/Info 6 0 R/Prev 141/W[1 2 2]/Index[2 1 4 1 6 1]/Filter/FlateDecode/DecodeParms<</Columns 5/Predictor 12>>/Length 22>>stream
84
84
  x\xDAcbdlg``b`\xB0\x04\x93\x93\x18\x18\x00\f\e\x01[
85
85
  endstream
86
86
  endobj
@@ -112,7 +112,7 @@ describe HexaPDF::Writer do
112
112
  HexaPDF::Writer.write(doc, output_io, incremental: true)
113
113
  assert_equal(output_io.string[0, @std_input_io.string.length], @std_input_io.string)
114
114
  doc = HexaPDF::Document.new(io: output_io)
115
- assert_equal(4, doc.revisions.size)
115
+ assert_equal(4, doc.revisions.count)
116
116
  assert_equal(2, doc.revisions.current.each.to_a.size)
117
117
  end
118
118
 
@@ -136,29 +136,58 @@ describe HexaPDF::Writer do
136
136
  end
137
137
  end
138
138
 
139
+ it "moves modified objects into the last revision" do
140
+ io = StringIO.new
141
+ io2 = StringIO.new
142
+
143
+ document = HexaPDF::Document.new
144
+ document.pages.add
145
+ HexaPDF::Writer.new(document, io).write
146
+
147
+ document = HexaPDF::Document.new(io: io)
148
+ document.pages.add
149
+ HexaPDF::Writer.new(document, io2).write_incremental
150
+
151
+ document = HexaPDF::Document.new(io: io2)
152
+ document.revisions.add
153
+ document.pages.add
154
+ HexaPDF::Writer.new(document, io).write
155
+
156
+ document = HexaPDF::Document.new(io: io)
157
+ assert_equal(3, document.revisions.count)
158
+ assert_equal(1, document.revisions.all[0].object(2)[:Kids].length)
159
+ assert_equal(2, document.revisions.all[1].object(2)[:Kids].length)
160
+ assert_equal(3, document.revisions.all[2].object(2)[:Kids].length)
161
+ end
162
+
139
163
  it "creates an xref stream if no xref stream is in a revision but object streams are" do
140
164
  document = HexaPDF::Document.new
141
165
  document.add({Type: :ObjStm})
142
166
  HexaPDF::Writer.new(document, StringIO.new).write
143
- assert(:XRef, document.object(2).type)
167
+ assert_equal(:XRef, document.object(4).type)
144
168
  end
145
169
 
146
170
  it "creates an xref stream if a previous revision had one" do
147
171
  document = HexaPDF::Document.new
148
172
  document.pages.add
149
- document.revisions.add
173
+ io = StringIO.new
174
+ HexaPDF::Writer.new(document, io).write
175
+
176
+ document = HexaPDF::Document.new(io: io)
150
177
  document.pages.add
151
178
  document.add({Type: :ObjStm})
152
- document.revisions.add
179
+ io2 = StringIO.new
180
+ HexaPDF::Writer.new(document, io2).write_incremental
181
+
182
+ document = HexaPDF::Document.new(io: io2)
153
183
  document.pages.add
154
- io = StringIO.new
155
- HexaPDF::Writer.new(document, io).write
184
+ HexaPDF::Writer.new(document, io).write_incremental
156
185
 
157
186
  document = HexaPDF::Document.new(io: io)
158
187
  assert_equal(3, document.revisions.count)
159
- assert(document.revisions[0].none? {|obj| obj.type == :XRef })
160
- assert(document.revisions[1].one? {|obj| obj.type == :XRef })
161
- assert(document.revisions[2].one? {|obj| obj.type == :XRef })
188
+ assert(document.revisions.all[0].none? {|obj| obj.type == :XRef })
189
+ assert(document.revisions.all[1].one? {|obj| obj.type == :XRef })
190
+ assert(document.revisions.all[2].one? {|obj| obj.type == :XRef })
162
191
  end
163
192
 
164
193
  it "raises an error if the class is misused and an xref section contains invalid entries" do
@@ -115,6 +115,16 @@ describe HexaPDF::Type::AcroForm::TextField do
115
115
  @field.flag(:password)
116
116
  assert_raises(HexaPDF::Error) { @field.field_value = 'test' }
117
117
  end
118
+
119
+ it "fails if it is a comb text field without a /MaxLen entry" do
120
+ @field.initialize_as_comb_text_field
121
+ assert_raises(HexaPDF::Error) { @field.field_value = 'test' }
122
+ end
123
+
124
+ it "fails if the value exceeds the length set by /MaxLen" do
125
+ @field[:MaxLen] = 5
126
+ assert_raises(HexaPDF::Error) { @field.field_value = 'testdf' }
127
+ end
118
128
  end
119
129
 
120
130
  it "sets and returns the default field value" do
@@ -197,5 +207,12 @@ describe HexaPDF::Type::AcroForm::TextField do
197
207
  @field[:V] = nil
198
208
  assert(@field.validate)
199
209
  end
210
+
211
+ it "checks that /MaxLen is set for comb text fields" do
212
+ @field.initialize_as_comb_text_field
213
+ refute(@field.validate)
214
+ @field[:MaxLen] = 2
215
+ assert(@field.validate)
216
+ end
200
217
  end
201
218
  end
@@ -21,6 +21,14 @@ describe HexaPDF::Type::Catalog do
21
21
  assert_equal(:Pages, pages.type)
22
22
  end
23
23
 
24
+ it "creates the name dictionary on access" do
25
+ assert_nil(@catalog[:Names])
26
+ names = @catalog.names
27
+ assert_equal(:XXNames, names.type)
28
+ other = @catalog.names
29
+ assert_same(other, names)
30
+ end
31
+
24
32
  describe "acro_form" do
25
33
  it "returns an existing form object" do
26
34
  @catalog[:AcroForm] = :test
@@ -0,0 +1,20 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'test_helper'
4
+ require 'hexapdf/document'
5
+ require 'hexapdf/type/names'
6
+
7
+ describe HexaPDF::Type::Names do
8
+ before do
9
+ @doc = HexaPDF::Document.new
10
+ @names = @doc.add({}, type: :XXNames)
11
+ end
12
+
13
+ it "returns the name tree for the /Dests entry" do
14
+ refute(@names.key?(:Dests))
15
+ dests = @names.destinations
16
+ assert_kind_of(HexaPDF::NameTreeNode, dests)
17
+ assert_same(dests, @names[:Dests])
18
+ assert_same(dests, @names.destinations)
19
+ end
20
+ end
@@ -88,10 +88,11 @@ describe HexaPDF::Type::XRefStream do
88
88
  @obj[:Index] = [0, 5]
89
89
  @obj[:W] = [1, 2, 2]
90
90
  dict = @obj.trailer
91
- assert_equal(3, dict.length)
91
+ assert_equal(4, dict.length)
92
92
  assert_equal(5, dict[:Size])
93
93
  assert_equal(["a", "b"], dict[:ID])
94
94
  assert_equal('x', dict[:Root])
95
+ assert_equal(:XRef, dict[:Type])
95
96
  end
96
97
  end
97
98
 
@@ -135,7 +135,17 @@ describe HexaPDF::Utils::SortedTreeNode do
135
135
  assert_nil(@root.find_entry('non'))
136
136
  end
137
137
 
138
- it "works when no entry exists" do
138
+ it "works when no entry exists and neither /Names nor /Kids are set" do
139
+ assert_nil(@root.find_entry('non'))
140
+ end
141
+
142
+ it "works when no entry exists and /Names is set" do
143
+ @root[:Names] = []
144
+ assert_nil(@root.find_entry('non'))
145
+ end
146
+
147
+ it "works when no entry exists and /Kids is set" do
148
+ @root[:Kids] = []
139
149
  assert_nil(@root.find_entry('non'))
140
150
  end
141
151
  end