hexapdf 0.22.0 → 0.23.0

Sign up to get free protection for your applications and to get access to all the features.
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