hexapdf 0.12.3 → 0.14.3

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 (103) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +132 -0
  3. data/examples/019-acro_form.rb +41 -4
  4. data/lib/hexapdf/cli/command.rb +4 -2
  5. data/lib/hexapdf/cli/image2pdf.rb +2 -1
  6. data/lib/hexapdf/cli/info.rb +51 -2
  7. data/lib/hexapdf/cli/inspect.rb +30 -8
  8. data/lib/hexapdf/cli/merge.rb +1 -1
  9. data/lib/hexapdf/cli/split.rb +74 -14
  10. data/lib/hexapdf/configuration.rb +15 -0
  11. data/lib/hexapdf/content/graphic_object/arc.rb +3 -3
  12. data/lib/hexapdf/dictionary.rb +12 -6
  13. data/lib/hexapdf/dictionary_fields.rb +2 -10
  14. data/lib/hexapdf/document.rb +41 -16
  15. data/lib/hexapdf/document/files.rb +0 -1
  16. data/lib/hexapdf/encryption/fast_arc4.rb +1 -1
  17. data/lib/hexapdf/encryption/security_handler.rb +1 -0
  18. data/lib/hexapdf/encryption/standard_security_handler.rb +1 -0
  19. data/lib/hexapdf/font/cmap.rb +1 -4
  20. data/lib/hexapdf/font/true_type/subsetter.rb +16 -3
  21. data/lib/hexapdf/font/true_type/table/head.rb +1 -0
  22. data/lib/hexapdf/font/true_type/table/os2.rb +2 -0
  23. data/lib/hexapdf/font/true_type/table/post.rb +15 -10
  24. data/lib/hexapdf/font_loader/from_configuration.rb +2 -2
  25. data/lib/hexapdf/font_loader/from_file.rb +18 -8
  26. data/lib/hexapdf/image_loader/png.rb +3 -2
  27. data/lib/hexapdf/importer.rb +3 -2
  28. data/lib/hexapdf/layout/line.rb +1 -1
  29. data/lib/hexapdf/layout/style.rb +23 -23
  30. data/lib/hexapdf/layout/text_layouter.rb +2 -2
  31. data/lib/hexapdf/layout/text_shaper.rb +3 -2
  32. data/lib/hexapdf/object.rb +52 -25
  33. data/lib/hexapdf/parser.rb +107 -7
  34. data/lib/hexapdf/pdf_array.rb +15 -5
  35. data/lib/hexapdf/revisions.rb +29 -21
  36. data/lib/hexapdf/serializer.rb +37 -10
  37. data/lib/hexapdf/task/optimize.rb +6 -4
  38. data/lib/hexapdf/tokenizer.rb +22 -0
  39. data/lib/hexapdf/type/acro_form/appearance_generator.rb +130 -27
  40. data/lib/hexapdf/type/acro_form/button_field.rb +5 -2
  41. data/lib/hexapdf/type/acro_form/choice_field.rb +68 -14
  42. data/lib/hexapdf/type/acro_form/field.rb +35 -5
  43. data/lib/hexapdf/type/acro_form/form.rb +139 -14
  44. data/lib/hexapdf/type/acro_form/text_field.rb +70 -4
  45. data/lib/hexapdf/type/actions/uri.rb +3 -2
  46. data/lib/hexapdf/type/annotations/widget.rb +3 -4
  47. data/lib/hexapdf/type/catalog.rb +2 -2
  48. data/lib/hexapdf/type/cid_font.rb +1 -1
  49. data/lib/hexapdf/type/file_specification.rb +1 -1
  50. data/lib/hexapdf/type/font.rb +1 -1
  51. data/lib/hexapdf/type/font_simple.rb +4 -2
  52. data/lib/hexapdf/type/font_true_type.rb +6 -2
  53. data/lib/hexapdf/type/font_type0.rb +4 -4
  54. data/lib/hexapdf/type/form.rb +6 -2
  55. data/lib/hexapdf/type/image.rb +2 -2
  56. data/lib/hexapdf/type/page.rb +21 -12
  57. data/lib/hexapdf/type/page_tree_node.rb +29 -5
  58. data/lib/hexapdf/type/resources.rb +5 -0
  59. data/lib/hexapdf/type/trailer.rb +2 -3
  60. data/lib/hexapdf/utils/object_hash.rb +0 -1
  61. data/lib/hexapdf/utils/sorted_tree_node.rb +18 -15
  62. data/lib/hexapdf/version.rb +1 -1
  63. data/test/hexapdf/common_tokenizer_tests.rb +2 -2
  64. data/test/hexapdf/content/graphic_object/test_arc.rb +4 -4
  65. data/test/hexapdf/content/test_canvas.rb +3 -3
  66. data/test/hexapdf/content/test_color_space.rb +1 -1
  67. data/test/hexapdf/encryption/test_aes.rb +4 -4
  68. data/test/hexapdf/encryption/test_standard_security_handler.rb +11 -11
  69. data/test/hexapdf/filter/test_ascii85_decode.rb +1 -1
  70. data/test/hexapdf/filter/test_ascii_hex_decode.rb +1 -1
  71. data/test/hexapdf/font/true_type/table/test_post.rb +1 -1
  72. data/test/hexapdf/font/true_type/test_subsetter.rb +10 -0
  73. data/test/hexapdf/font_loader/test_from_configuration.rb +7 -3
  74. data/test/hexapdf/font_loader/test_from_file.rb +7 -0
  75. data/test/hexapdf/layout/test_text_layouter.rb +12 -5
  76. data/test/hexapdf/test_configuration.rb +2 -2
  77. data/test/hexapdf/test_dictionary.rb +8 -1
  78. data/test/hexapdf/test_dictionary_fields.rb +9 -2
  79. data/test/hexapdf/test_document.rb +18 -10
  80. data/test/hexapdf/test_object.rb +71 -26
  81. data/test/hexapdf/test_parser.rb +205 -51
  82. data/test/hexapdf/test_pdf_array.rb +8 -1
  83. data/test/hexapdf/test_revisions.rb +35 -0
  84. data/test/hexapdf/test_serializer.rb +7 -0
  85. data/test/hexapdf/test_tokenizer.rb +28 -0
  86. data/test/hexapdf/test_writer.rb +2 -2
  87. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +288 -35
  88. data/test/hexapdf/type/acro_form/test_button_field.rb +15 -0
  89. data/test/hexapdf/type/acro_form/test_choice_field.rb +92 -9
  90. data/test/hexapdf/type/acro_form/test_field.rb +39 -0
  91. data/test/hexapdf/type/acro_form/test_form.rb +87 -15
  92. data/test/hexapdf/type/acro_form/test_text_field.rb +77 -1
  93. data/test/hexapdf/type/test_font_simple.rb +2 -1
  94. data/test/hexapdf/type/test_font_true_type.rb +6 -0
  95. data/test/hexapdf/type/test_form.rb +8 -1
  96. data/test/hexapdf/type/test_page.rb +8 -1
  97. data/test/hexapdf/type/test_page_tree_node.rb +42 -0
  98. data/test/hexapdf/type/test_resources.rb +6 -0
  99. data/test/hexapdf/utils/test_bit_field.rb +2 -0
  100. data/test/hexapdf/utils/test_object_hash.rb +5 -0
  101. data/test/hexapdf/utils/test_sorted_tree_node.rb +10 -9
  102. data/test/test_helper.rb +2 -0
  103. metadata +6 -12
@@ -99,6 +99,12 @@ describe HexaPDF::Type::AcroForm::Field do
99
99
  refute(@field.terminal_field?)
100
100
  end
101
101
 
102
+ it "can check whether a widget is embedded in the field" do
103
+ refute(@field.embedded_widget?)
104
+ @field[:Subtype] = :Wdiget
105
+ assert(@field.embedded_widget?)
106
+ end
107
+
102
108
  describe "each_widget" do
103
109
  it "yields a wrapped instance of self if a single widget is embedded" do
104
110
  @field[:Subtype] = :Widget
@@ -175,6 +181,39 @@ describe HexaPDF::Type::AcroForm::Field do
175
181
  end
176
182
  end
177
183
 
184
+ describe "delete_widget" do
185
+ before do
186
+ @page = @doc.pages.add
187
+ end
188
+
189
+ it "does nothing if the provided widget doesn't belong to the field" do
190
+ wrong_widget = @doc.add({Subtype: :Widget})
191
+
192
+ @field.create_widget(@page)
193
+ @field.delete_widget(wrong_widget)
194
+ assert_equal(:Widget, @field[:Subtype])
195
+
196
+ @field.create_widget(@page)
197
+ @field.delete_widget(wrong_widget)
198
+ assert_equal(2, @field[:Kids].size)
199
+ end
200
+
201
+ it "deletes the widget if it is embedded" do
202
+ widget = @field.create_widget(@page)
203
+ @field.delete_widget(widget)
204
+ refute(@field.key?(:Subtype))
205
+ assert(@page[:Annots].empty?)
206
+ end
207
+
208
+ it "deletes the widget if it is not embedded" do
209
+ @field.create_widget(@page)
210
+ widget2 = @field.create_widget(@page)
211
+ @field.delete_widget(widget2)
212
+ assert_equal(1, @field[:Kids].size)
213
+ assert_equal(@field[:Kids].value, @page[:Annots].value)
214
+ end
215
+ end
216
+
178
217
  describe "perform_validation" do
179
218
  before do
180
219
  @field[:FT] = :Tx
@@ -7,7 +7,7 @@ require 'hexapdf/type/acro_form/form'
7
7
  describe HexaPDF::Type::AcroForm::Form do
8
8
  before do
9
9
  @doc = HexaPDF::Document.new
10
- @acro_form = @doc.add({}, type: :XXAcroForm)
10
+ @acro_form = @doc.add({Fields: []}, type: :XXAcroForm)
11
11
  end
12
12
 
13
13
  describe "signature flags" do
@@ -49,7 +49,7 @@ describe HexaPDF::Type::AcroForm::Form do
49
49
  root_fields = @acro_form.find_root_fields
50
50
  assert_equal(result, root_fields.map(&:value))
51
51
  assert_kind_of(HexaPDF::Type::AcroForm::TextField, root_fields[0])
52
- refute(@acro_form.key?(:Fields))
52
+ assert_equal([], @acro_form[:Fields].value)
53
53
 
54
54
  @acro_form.find_root_fields!
55
55
  assert_equal(result, @acro_form[:Fields].value.map(&:value))
@@ -115,6 +115,10 @@ describe HexaPDF::Type::AcroForm::Form do
115
115
  end
116
116
 
117
117
  describe "create fields" do
118
+ before do
119
+ @acro_form = @doc.acro_form(create: true)
120
+ end
121
+
118
122
  describe "handles the general case" do
119
123
  it "works for names with a dot" do
120
124
  @acro_form[:Fields] = [{T: "root"}]
@@ -134,9 +138,57 @@ describe HexaPDF::Type::AcroForm::Form do
134
138
  end
135
139
  end
136
140
 
141
+ def applies_variable_text_properties(method, **args)
142
+ field = @acro_form.send(method, "field", **args, font: 'Times')
143
+ font_name, font_size = field.parse_default_appearance_string
144
+ assert_equal(:'Times-Roman', @acro_form.default_resources.font(font_name)[:BaseFont])
145
+ assert_equal(0, font_size)
146
+
147
+ field = @acro_form.send(method, "field", **args, font_size: 10)
148
+ font_name, font_size = field.parse_default_appearance_string
149
+ assert_equal(:Helvetica, @acro_form.default_resources.font(font_name)[:BaseFont])
150
+ assert_equal(10, font_size)
151
+
152
+ field = @acro_form.send(method, "field", **args, font: 'Courier', font_size: 10, align: :center)
153
+ font_name, font_size = field.parse_default_appearance_string
154
+ assert_equal(:Courier, @acro_form.default_resources.font(font_name)[:BaseFont])
155
+ assert_equal(10, font_size)
156
+ assert_equal(:center, field.text_alignment)
157
+ end
158
+
137
159
  it "creates a text field" do
138
160
  field = @acro_form.create_text_field("field")
139
161
  assert_equal(:Tx, field.field_type)
162
+ applies_variable_text_properties(:create_text_field)
163
+ end
164
+
165
+ it "creates a multiline text field" do
166
+ field = @acro_form.create_multiline_text_field("field")
167
+ assert_equal(:Tx, field.field_type)
168
+ assert(field.multiline_text_field?)
169
+ applies_variable_text_properties(:create_multiline_text_field)
170
+ end
171
+
172
+ it "creates a comb text field" do
173
+ field = @acro_form.create_comb_text_field("field", max_chars: 9)
174
+ assert_equal(:Tx, field.field_type)
175
+ assert_equal(9, field[:MaxLen])
176
+ assert(field.comb_text_field?)
177
+ applies_variable_text_properties(:create_comb_text_field, max_chars: 9)
178
+ end
179
+
180
+ it "creates a password field" do
181
+ field = @acro_form.create_password_field("field")
182
+ assert_equal(:Tx, field.field_type)
183
+ assert(field.password_field?)
184
+ applies_variable_text_properties(:create_password_field)
185
+ end
186
+
187
+ it "creates a file select field" do
188
+ field = @acro_form.create_file_select_field("field")
189
+ assert_equal(:Tx, field.field_type)
190
+ assert(field.file_select_field?)
191
+ applies_variable_text_properties(:create_file_select_field)
140
192
  end
141
193
 
142
194
  it "creates a check box" do
@@ -150,13 +202,19 @@ describe HexaPDF::Type::AcroForm::Form do
150
202
  end
151
203
 
152
204
  it "creates a combo box" do
153
- field = @acro_form.create_combo_box("field")
205
+ field = @acro_form.create_combo_box("field", option_items: ['a', 'b', 'c'], editable: true)
154
206
  assert(field.combo_box?)
207
+ assert_equal(['a', 'b', 'c'], field.option_items)
208
+ assert(field.flagged?(:edit))
209
+ applies_variable_text_properties(:create_combo_box)
155
210
  end
156
211
 
157
212
  it "creates a list box" do
158
- field = @acro_form.create_list_box("field")
213
+ field = @acro_form.create_list_box("field", option_items: ['a', 'b', 'c'], multi_select: true)
159
214
  assert(field.list_box?)
215
+ assert_equal(['a', 'b', 'c'], field.option_items)
216
+ assert(field.flagged?(:multi_select))
217
+ applies_variable_text_properties(:create_list_box)
160
218
  end
161
219
  end
162
220
 
@@ -185,15 +243,29 @@ describe HexaPDF::Type::AcroForm::Form do
185
243
  assert(@acro_form[:NeedAppearances])
186
244
  end
187
245
 
188
- it "creates the appearances of all field widgets if necessary" do
189
- tf = @acro_form.create_text_field('test')
190
- tf.set_default_appearance_string
191
- tf.create_widget(@doc.pages.add)
192
- cb = @acro_form.create_check_box('test2')
193
- cb.create_widget(@doc.pages.add)
194
- @acro_form.create_appearances
195
- assert(tf.each_widget.all? {|w| w.appearance.normal_appearance.kind_of?(HexaPDF::Stream) })
196
- assert(cb.each_widget.all? {|w| w.appearance.normal_appearance[:Yes].kind_of?(HexaPDF::Stream) })
246
+ describe "create_appearances" do
247
+ before do
248
+ @tf = @acro_form.create_text_field('test')
249
+ @tf.set_default_appearance_string
250
+ @tf.create_widget(@doc.pages.add)
251
+ @cb = @acro_form.create_check_box('test2')
252
+ @cb.create_widget(@doc.pages.add)
253
+ end
254
+
255
+ it "creates the appearances of all field widgets if necessary" do
256
+ @acro_form.create_appearances
257
+ assert(@tf.each_widget.all? {|w| w.appearance.normal_appearance.kind_of?(HexaPDF::Stream) })
258
+ assert(@cb.each_widget.all? {|w| w.appearance.normal_appearance[:Yes].kind_of?(HexaPDF::Stream) })
259
+ end
260
+
261
+ it "force the creation of appearances if force is true" do
262
+ @acro_form.create_appearances
263
+ text_stream = @tf[:AP][:N].raw_stream
264
+ @acro_form.create_appearances
265
+ assert_same(text_stream, @tf[:AP][:N].raw_stream)
266
+ @acro_form.create_appearances(force: true)
267
+ refute_same(text_stream, @tf[:AP][:N].raw_stream)
268
+ end
197
269
  end
198
270
 
199
271
  describe "perform_validation" do
@@ -204,9 +276,9 @@ describe HexaPDF::Type::AcroForm::Form do
204
276
 
205
277
  it "checks whether the font used in /DA is available in /DR" do
206
278
  @acro_form[:DA] = '/F2 0 Tf /F1 0 Tf'
207
- refute(@acro_form.validate {|msg, c| assert_match(/DR must also be present/, msg) })
279
+ refute(@acro_form.validate {|msg| assert_match(/DR must also be present/, msg) })
208
280
  @acro_form.default_resources[:Font] = {}
209
- refute(@acro_form.validate {|msg, c| assert_match(/font.*is not.*resource/, msg) })
281
+ refute(@acro_form.validate {|msg| assert_match(/font.*is not.*resource/, msg) })
210
282
  @acro_form.default_resources[:Font][:F1] = :yes
211
283
  assert(@acro_form.validate)
212
284
  end
@@ -21,6 +21,56 @@ describe HexaPDF::Type::AcroForm::TextField do
21
21
  assert_equal(6, @field[:MaxLen])
22
22
  end
23
23
 
24
+ it "can be initialized as a multiline text field" do
25
+ @field.flag(:comb)
26
+ @field.initialize_as_multiline_text_field
27
+ assert(@field.multiline_text_field?)
28
+ end
29
+
30
+ it "can be initialized as comb text field" do
31
+ @field.flag(:multiline)
32
+ @field.initialize_as_comb_text_field
33
+ assert(@field.comb_text_field?)
34
+ end
35
+
36
+ it "can be initialized as password field" do
37
+ @field.flag(:multiline)
38
+ @field[:V] = 'test'
39
+ @field.initialize_as_password_field
40
+ assert_nil(@field[:V])
41
+ assert(@field.password_field?)
42
+ end
43
+
44
+ it "can be initialized as a file select field" do
45
+ @field.flag(:multiline)
46
+ @field.initialize_as_file_select_field
47
+ assert(@field.file_select_field?)
48
+ end
49
+
50
+ it "can check whether the field is a multiline text field" do
51
+ refute(@field.multiline_text_field?)
52
+ @field.flag(:multiline)
53
+ assert(@field.multiline_text_field?)
54
+ end
55
+
56
+ it "can check whether the field is a comb text field" do
57
+ refute(@field.comb_text_field?)
58
+ @field.flag(:comb)
59
+ assert(@field.comb_text_field?)
60
+ end
61
+
62
+ it "can check whether the field is a password field" do
63
+ refute(@field.password_field?)
64
+ @field.flag(:password)
65
+ assert(@field.password_field?)
66
+ end
67
+
68
+ it "can check whether the field is a file select field" do
69
+ refute(@field.file_select_field?)
70
+ @field.flag(:file_select)
71
+ assert(@field.file_select_field?)
72
+ end
73
+
24
74
  describe "field_value" do
25
75
  it "handles unset values" do
26
76
  assert_nil(@field.field_value)
@@ -81,14 +131,38 @@ describe HexaPDF::Type::AcroForm::TextField do
81
131
  end
82
132
 
83
133
  describe "create_appearances" do
84
- it "creates the needed streams" do
134
+ before do
85
135
  @doc.acro_form(create: true)
86
136
  @field.create_widget(@doc.pages.add, Rect: [0, 0, 0, 0])
87
137
  @field.set_default_appearance_string
138
+ end
139
+
140
+ it "creates the needed streams" do
88
141
  @field.create_appearances
89
142
  assert(@field[:AP][:N])
90
143
  end
91
144
 
145
+ it "doesn't create a new appearance stream if the field value hasn't changed, checked per widget" do
146
+ @field.create_appearances
147
+ stream = @field[:AP][:N].raw_stream
148
+ @field.create_appearances
149
+ assert_same(stream, @field[:AP][:N].raw_stream)
150
+ @field.field_value = 'test'
151
+ refute_same(stream, @field[:AP][:N].raw_stream)
152
+
153
+ widget = @field.create_widget(@doc.pages.add, Rect: [0, 0, 0, 0])
154
+ assert_nil(widget[:AP])
155
+ @field.create_appearances
156
+ refute_nil(widget[:AP][:N])
157
+ end
158
+
159
+ it "always creates a new appearance stream if force is true" do
160
+ @field.create_appearances
161
+ stream = @field[:AP][:N].raw_stream
162
+ @field.create_appearances(force: true)
163
+ refute_same(stream, @field[:AP][:N].raw_stream)
164
+ end
165
+
92
166
  it "uses the configuration option acro_form.appearance_generator" do
93
167
  @doc.config['acro_form.appearance_generator'] = 'NonExistent'
94
168
  assert_raises(Exception) { @field.create_appearances }
@@ -114,6 +188,8 @@ describe HexaPDF::Type::AcroForm::TextField do
114
188
  assert(@field.validate)
115
189
  @field[:MaxLen] = 2
116
190
  refute(@field.validate)
191
+ @field[:V] = nil
192
+ assert(@field.validate)
117
193
  end
118
194
  end
119
195
  end
@@ -161,8 +161,9 @@ describe HexaPDF::Type::FontSimple do
161
161
  end
162
162
 
163
163
  describe "validation" do
164
+ before { assert(@font.validate) }
165
+
164
166
  it "validates the existence of required keys" do
165
- assert(@font.validate)
166
167
  @font.delete(:FirstChar)
167
168
  refute(@font.validate)
168
169
  end
@@ -16,6 +16,12 @@ describe HexaPDF::Type::FontTrueType do
16
16
  end
17
17
 
18
18
  describe "validation" do
19
+ it "ignores some missing fields if the font name is one of the standard PDF fonts" do
20
+ @font[:BaseFont] = :'Arial,Bold'
21
+ [:FirstChar, :LastChar, :Widths, :FontDescriptor].each {|field| @font.delete(field) }
22
+ assert(@font.validate)
23
+ end
24
+
19
25
  it "requires that the FontDescriptor key is set" do
20
26
  assert(@font.validate)
21
27
  @font.delete(:FontDescriptor)
@@ -43,13 +43,20 @@ describe HexaPDF::Type::Form do
43
43
  @form.contents = 'test'
44
44
  assert_equal('test', @form.stream)
45
45
  end
46
+
47
+ it "clears the cache to make sure that a new canvas can be created" do
48
+ @form[:BBox] = [0, 0, 100, 100]
49
+ canvas = @form.canvas
50
+ @form.contents = ''
51
+ refute_same(canvas, @form.canvas)
52
+ end
46
53
  end
47
54
 
48
55
  describe "resources" do
49
56
  it "creates the resource dictionary if it is not found" do
50
57
  resources = @form.resources
51
58
  assert_equal(:XXResources, resources.type)
52
- assert_equal({}, resources.value)
59
+ assert_equal({ProcSet: [:PDF, :Text, :ImageB, :ImageC, :ImageI]}, resources.value)
53
60
  end
54
61
 
55
62
  it "returns the already used resource dictionary" do
@@ -277,7 +277,7 @@ describe HexaPDF::Type::Page do
277
277
  page = @doc.add({Type: :Page, Parent: @doc.pages.root})
278
278
  resources = page.resources
279
279
  assert_equal(:XXResources, resources.type)
280
- assert_equal({}, resources.value)
280
+ assert_equal({ProcSet: [:PDF, :Text, :ImageB, :ImageC, :ImageI]}, resources.value)
281
281
  end
282
282
 
283
283
  it "returns the already used resource dictionary" do
@@ -319,6 +319,13 @@ describe HexaPDF::Type::Page do
319
319
  end
320
320
  end
321
321
 
322
+ it "returns all ancestor page tree nodes of a page" do
323
+ root = @doc.add({Type: :Pages})
324
+ kid = @doc.add({Type: :Pages, Parent: root})
325
+ page = @doc.add({Type: :Page, Parent: kid})
326
+ assert_equal([kid, root], page.ancestor_nodes)
327
+ end
328
+
322
329
  describe "canvas" do
323
330
  before do
324
331
  @page = @doc.pages.add
@@ -206,6 +206,48 @@ describe HexaPDF::Type::PageTreeNode do
206
206
  end
207
207
  end
208
208
 
209
+ describe "move_page" do
210
+ before do
211
+ define_multilevel_page_tree
212
+ end
213
+
214
+ it "moves the page to the first place" do
215
+ @root.move_page(@pages[1], 0)
216
+ assert_equal([@pages[1], @pages[0], *@pages[2..-1]], @root.each_page.to_a)
217
+ assert(@root.validate)
218
+ end
219
+
220
+ it "moves the page to the correct location with a positive index" do
221
+ @root.move_page(1, 3)
222
+ assert_equal([@pages[0], @pages[2], @pages[1], *@pages[3..-1]], @root.each_page.to_a)
223
+ assert(@root.validate)
224
+ end
225
+
226
+ it "moves the page to the last place" do
227
+ @root.move_page(1, -1)
228
+ assert_equal([@pages[0], *@pages[2..-1], @pages[1]], @root.each_page.to_a)
229
+ assert(@root.validate)
230
+ end
231
+
232
+ it "fails if the index to the moving page is invalid" do
233
+ assert_raises(HexaPDF::Error) { @root.move_page(10, 0) }
234
+ end
235
+
236
+ it "fails if the moving page was deleted/is null" do
237
+ @doc.delete(@pages[0])
238
+ assert_raises(HexaPDF::Error) { @root.move_page(@pages[0], 3) }
239
+ end
240
+
241
+ it "fails if the page was not yet added to a page tree" do
242
+ page = @doc.add({Type: :Page})
243
+ assert_raises(HexaPDF::Error) { @root.move_page(page, 3) }
244
+ end
245
+
246
+ it "fails if the page is not part of the page tree" do
247
+ assert_raises(HexaPDF::Error) { @kid1.move_page(@pages[6], 3) }
248
+ end
249
+ end
250
+
209
251
  describe "each_page" do
210
252
  before do
211
253
  define_multilevel_page_tree
@@ -194,6 +194,12 @@ describe HexaPDF::Type::Resources do
194
194
  assert_equal([:PDF, :Text, :ImageB, :ImageC, :ImageI], @res[:ProcSet].value)
195
195
  end
196
196
 
197
+ it "handles an invalid ProcSet containing a single value instead of an array" do
198
+ @res[:ProcSet] = :PDF
199
+ @res.validate
200
+ assert_equal([:PDF], @res[:ProcSet].value)
201
+ end
202
+
197
203
  it "removes invalid procedure set names from ProcSet" do
198
204
  @res[:ProcSet] = [:PDF, :Unknown]
199
205
  @res.validate