hexapdf 0.27.0 → 0.28.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 (62) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +59 -1
  3. data/examples/019-acro_form.rb +14 -3
  4. data/examples/023-images.rb +30 -0
  5. data/lib/hexapdf/cli/info.rb +5 -1
  6. data/lib/hexapdf/cli/inspect.rb +2 -2
  7. data/lib/hexapdf/cli/split.rb +2 -2
  8. data/lib/hexapdf/configuration.rb +1 -2
  9. data/lib/hexapdf/content/canvas.rb +8 -3
  10. data/lib/hexapdf/dictionary.rb +1 -5
  11. data/lib/hexapdf/document.rb +6 -10
  12. data/lib/hexapdf/filter/ascii85_decode.rb +1 -1
  13. data/lib/hexapdf/importer.rb +32 -27
  14. data/lib/hexapdf/layout/list_box.rb +1 -5
  15. data/lib/hexapdf/object.rb +5 -0
  16. data/lib/hexapdf/parser.rb +13 -0
  17. data/lib/hexapdf/revision.rb +15 -12
  18. data/lib/hexapdf/revisions.rb +4 -0
  19. data/lib/hexapdf/tokenizer.rb +14 -8
  20. data/lib/hexapdf/type/acro_form/appearance_generator.rb +174 -128
  21. data/lib/hexapdf/type/acro_form/button_field.rb +5 -3
  22. data/lib/hexapdf/type/acro_form/choice_field.rb +2 -0
  23. data/lib/hexapdf/type/acro_form/field.rb +11 -5
  24. data/lib/hexapdf/type/acro_form/form.rb +33 -7
  25. data/lib/hexapdf/type/acro_form/signature_field.rb +2 -0
  26. data/lib/hexapdf/type/acro_form/text_field.rb +12 -2
  27. data/lib/hexapdf/type/annotations/widget.rb +3 -0
  28. data/lib/hexapdf/type/font_true_type.rb +14 -0
  29. data/lib/hexapdf/type/object_stream.rb +2 -2
  30. data/lib/hexapdf/type/outline.rb +1 -1
  31. data/lib/hexapdf/type/page.rb +56 -46
  32. data/lib/hexapdf/version.rb +1 -1
  33. data/lib/hexapdf/writer.rb +2 -3
  34. data/test/hexapdf/content/test_canvas.rb +5 -0
  35. data/test/hexapdf/document/test_pages.rb +2 -2
  36. data/test/hexapdf/encryption/test_aes.rb +1 -1
  37. data/test/hexapdf/filter/test_predictor.rb +0 -1
  38. data/test/hexapdf/layout/test_box.rb +2 -1
  39. data/test/hexapdf/layout/test_column_box.rb +1 -1
  40. data/test/hexapdf/layout/test_list_box.rb +1 -1
  41. data/test/hexapdf/test_document.rb +2 -8
  42. data/test/hexapdf/test_importer.rb +13 -6
  43. data/test/hexapdf/test_parser.rb +17 -0
  44. data/test/hexapdf/test_revision.rb +15 -14
  45. data/test/hexapdf/test_revisions.rb +43 -0
  46. data/test/hexapdf/test_stream.rb +1 -1
  47. data/test/hexapdf/test_tokenizer.rb +3 -4
  48. data/test/hexapdf/test_writer.rb +3 -3
  49. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +135 -56
  50. data/test/hexapdf/type/acro_form/test_button_field.rb +6 -1
  51. data/test/hexapdf/type/acro_form/test_choice_field.rb +4 -0
  52. data/test/hexapdf/type/acro_form/test_field.rb +4 -4
  53. data/test/hexapdf/type/acro_form/test_form.rb +18 -0
  54. data/test/hexapdf/type/acro_form/test_signature_field.rb +4 -0
  55. data/test/hexapdf/type/acro_form/test_text_field.rb +13 -0
  56. data/test/hexapdf/type/signature/common.rb +3 -1
  57. data/test/hexapdf/type/test_font_true_type.rb +20 -0
  58. data/test/hexapdf/type/test_object_stream.rb +2 -1
  59. data/test/hexapdf/type/test_outline.rb +3 -0
  60. data/test/hexapdf/type/test_page.rb +67 -30
  61. data/test/hexapdf/type/test_page_tree_node.rb +4 -2
  62. metadata +46 -3
@@ -18,12 +18,6 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
18
18
  @generator = HexaPDF::Type::AcroForm::AppearanceGenerator.new(@widget)
19
19
  end
20
20
 
21
- it "fails for unsupported button fields" do
22
- @field.flag(:push_button)
23
- @generator = HexaPDF::Type::AcroForm::AppearanceGenerator.new(@widget)
24
- assert_raises(HexaPDF::Error) { @generator.create_appearances }
25
- end
26
-
27
21
  it "fails for unsupported field types" do
28
22
  @field[:FT] = :Unknown
29
23
  assert_raises(HexaPDF::Error) { @generator.create_appearances }
@@ -141,8 +135,8 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
141
135
  end
142
136
 
143
137
  def execute
144
- @generator.send(:draw_marker, @xform.canvas, @widget[:Rect], @widget.border_style.width,
145
- @widget.marker_style)
138
+ @generator.send(:draw_marker, @xform.canvas, @widget[:Rect].width, @widget[:Rect].height,
139
+ @widget.border_style.width, @widget.marker_style)
146
140
  end
147
141
 
148
142
  it "handles the marker :circle specially for radio button widgets" do
@@ -196,6 +190,18 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
196
190
  [:show_text, ["4"]],
197
191
  [:end_text]])
198
192
  end
193
+
194
+ it "draws the default marker if an empty string is specified as marker" do
195
+ @widget.marker_style(style: '', color: 0.5, size: 5)
196
+ execute
197
+ assert_operators(@xform.stream,
198
+ [[:set_font_and_size, [:F1, 5]],
199
+ [:set_device_gray_non_stroking_color, [0.5]],
200
+ [:begin_text],
201
+ [:set_text_matrix, [1, 0, 0, 1, 2.885, 8.2725]],
202
+ [:show_text, ["4"]],
203
+ [:end_text]])
204
+ end
199
205
  end
200
206
  end
201
207
 
@@ -213,7 +219,15 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
213
219
 
214
220
  it "updates the widgets' /AS entry to point to the selected appearance" do
215
221
  @generator.create_appearances
216
- assert_equal(@field[:V], @widget[:AS])
222
+ assert_equal(:Off, @widget[:AS])
223
+
224
+ @field.field_value = :Yes
225
+ @generator.create_appearances
226
+ assert_equal(:Yes, @widget[:AS])
227
+
228
+ @field.delete(:V)
229
+ @generator.create_appearances
230
+ assert_equal(:Off, @widget[:AS])
217
231
  end
218
232
 
219
233
  it "set the print flag on the widgets" do
@@ -231,6 +245,33 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
231
245
  assert_equal(12, @widget[:Rect].height)
232
246
  end
233
247
 
248
+ describe "takes the rotation into account" do
249
+ def check_rotation(angle, width, height, matrix)
250
+ @widget[:MK] = {R: angle}
251
+ @field[:V] = :Yes
252
+ @generator.create_appearances
253
+ form = @widget[:AP][:N][@widget[:AS]]
254
+ assert_equal([0, 0, width, height], form[:BBox].value)
255
+ assert_equal(matrix, form[:Matrix].value)
256
+ end
257
+
258
+ it "works for 0 degrees" do
259
+ check_rotation(-360, @widget[:Rect].width, @widget[:Rect].height, [1, 0, 0, 1, 0, 0])
260
+ end
261
+
262
+ it "works for 90 degrees" do
263
+ check_rotation(450, @widget[:Rect].height, @widget[:Rect].width, [0, 1, -1, 0, 0, 0])
264
+ end
265
+
266
+ it "works for 180 degrees" do
267
+ check_rotation(180, @widget[:Rect].width, @widget[:Rect].height, [0, -1, -1, 0, 0, 0])
268
+ end
269
+
270
+ it "works for 270 degrees" do
271
+ check_rotation(-90, @widget[:Rect].height, @widget[:Rect].width, [0, -1, 1, 0, 0, 0])
272
+ end
273
+ end
274
+
234
275
  it "creates the needed appearance streams" do
235
276
  @widget[:AP][:N].delete(:Off)
236
277
  @generator.create_appearances
@@ -292,61 +333,21 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
292
333
  assert_equal(:Off, @widget[:AS])
293
334
  end
294
335
 
295
- it "set the print flag on the widgets" do
296
- @generator.create_appearances
297
- assert(@widget.flagged?(:print))
298
- end
299
-
300
- it "adjusts the /Rect if width is zero" do
301
- @generator.create_appearances
302
- assert_equal(12, @widget[:Rect].width)
303
- end
304
-
305
- it "adjusts the /Rect if height is zero" do
306
- @generator.create_appearances
307
- assert_equal(12, @widget[:Rect].height)
308
- end
309
-
310
336
  it "creates the needed appearance streams" do
311
337
  @generator.create_appearances
312
338
  assert_equal(:XObject, @widget[:AP][:N][:Off].type)
313
339
  assert_equal(:XObject, @widget[:AP][:N][:radio].type)
314
340
  end
341
+ end
315
342
 
316
- it "creates the /Off appearance stream" do
317
- @widget.marker_style(style: :cross)
318
- @generator.create_appearances
319
- assert_operators(@widget[:AP][:N][:Off].stream,
320
- [[:save_graphics_state],
321
- [:set_device_gray_non_stroking_color, [1.0]],
322
- [:append_rectangle, [0, 0, 12, 12]],
323
- [:fill_path_non_zero],
324
- [:append_rectangle, [0.5, 0.5, 11, 11]],
325
- [:stroke_path], [:restore_graphics_state]])
326
- end
327
-
328
- it "creates the appearance stream according to the set value" do
329
- @widget.marker_style(style: :check)
330
- @generator.create_appearances
331
- assert_operators(@widget[:AP][:N][:radio].stream,
332
- [[:save_graphics_state],
333
- [:set_device_gray_non_stroking_color, [1.0]],
334
- [:append_rectangle, [0, 0, 12, 12]],
335
- [:fill_path_non_zero],
336
- [:append_rectangle, [0.5, 0.5, 11, 11]],
337
- [:stroke_path], [:restore_graphics_state],
338
-
339
- [:save_graphics_state],
340
- [:set_font_and_size, [:F1, 10]],
341
- [:begin_text],
342
- [:set_text_matrix, [1, 0, 0, 1, 1.77, 2.545]],
343
- [:show_text, ["4"]],
344
- [:end_text],
345
- [:restore_graphics_state]])
343
+ describe "push buttons" do
344
+ before do
345
+ @field.initialize_as_push_button
346
+ @widget = @field.create_widget(@page, Rect: [0, 0, 0, 0])
347
+ @generator = HexaPDF::Type::AcroForm::AppearanceGenerator.new(@widget)
346
348
  end
347
349
 
348
- it "fails if the appearance dictionaries are not set up" do
349
- @widget[:AP][:N].delete(:radio)
350
+ it "fails because it is not implemented yet" do
350
351
  assert_raises(HexaPDF::Error) { @generator.create_appearances }
351
352
  end
352
353
  end
@@ -417,6 +418,37 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
417
418
  assert_match(/test1/, form.contents)
418
419
  end
419
420
 
421
+ describe "takes the rotation into account" do
422
+ before do
423
+ @widget[:Rect] = [0, 0, 100, 20]
424
+ end
425
+
426
+ def check_rotation(angle, width, height, matrix)
427
+ @widget[:MK] = {R: angle}
428
+ @field[:V] = 'test'
429
+ @generator.create_appearances
430
+ form = @widget[:AP][:N]
431
+ assert_equal([0, 0, width, height], form[:BBox].value)
432
+ assert_equal(matrix, form[:Matrix].value)
433
+ end
434
+
435
+ it "works for 0 degrees" do
436
+ check_rotation(-360, @widget[:Rect].width, @widget[:Rect].height, [1, 0, 0, 1, 0, 0])
437
+ end
438
+
439
+ it "works for 90 degrees" do
440
+ check_rotation(450, @widget[:Rect].height, @widget[:Rect].width, [0, 1, -1, 0, 0, 0])
441
+ end
442
+
443
+ it "works for 180 degrees" do
444
+ check_rotation(180, @widget[:Rect].width, @widget[:Rect].height, [0, -1, -1, 0, 0, 0])
445
+ end
446
+
447
+ it "works for 270 degrees" do
448
+ check_rotation(-90, @widget[:Rect].height, @widget[:Rect].width, [0, -1, 1, 0, 0, 0])
449
+ end
450
+ end
451
+
420
452
  describe "font size calculation" do
421
453
  before do
422
454
  @widget[:Rect].height = 20
@@ -496,6 +528,53 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
496
528
  end
497
529
  end
498
530
 
531
+ describe "Javascript action AFNumber_Format" do
532
+ before do
533
+ @field.field_value = '1234567.898765'
534
+ @action = {S: :JavaScript, JS: ''}
535
+ @field[:AA] = {F: @action}
536
+ end
537
+
538
+ def assert_format(arg_string, result, range)
539
+ @action[:JS] = "AFNumber_Format(#{arg_string});"
540
+ @generator.create_appearances
541
+ assert_operators(@widget[:AP][:N].stream, result, range: range)
542
+ end
543
+
544
+ it "respects the set number of decimals" do
545
+ assert_format('0, 2, 0, 0, "E", false',
546
+ [:show_text, ["1.234.568E"]], 9)
547
+ assert_format('2, 2, 0, 0, "E", false',
548
+ [:show_text, ["1.234.567,90E"]], 9)
549
+ end
550
+
551
+ it "respects the digit separator style" do
552
+ ["1,234,567.90", "1234567.90", "1.234.567,90", "1234567,90"].each_with_index do |result, style|
553
+ assert_format("2, #{style}, 0, 0, \"\", false", [:show_text, [result]], 9)
554
+ end
555
+ end
556
+
557
+ it "respects the negative value styling" do
558
+ @field.field_value = '-1234567.898'
559
+ ["-E1234567,90", "E1234567,90", "(E1234567,90)", "(E1234567,90)"].each_with_index do |result, style|
560
+ assert_format("2, 3, #{style}, 0, \"E\", true",
561
+ [[:set_device_rgb_non_stroking_color, [style % 2, 0.0, 0.0]],
562
+ [:begin_text],
563
+ [:set_text_matrix, [1, 0, 0, 1, 2, 3.240724]],
564
+ [:show_text, [result]]], 6..9)
565
+ end
566
+ end
567
+
568
+ it "respects the specified currency string and position" do
569
+ assert_format('2, 3, 0, 0, " E", false', [:show_text, ["1234567,90 E"]], 9)
570
+ assert_format('2, 3, 0, 0, "E ", true', [:show_text, ["E 1234567,90"]], 9)
571
+ end
572
+
573
+ it "does nothing to the value if the Javascript method could not be determined " do
574
+ assert_format('2, 3, 0, 0, " E", false, a', [:show_text, ["1234567.898765"]], 8)
575
+ end
576
+ end
577
+
499
578
  it "creates the /N appearance stream according to the set string" do
500
579
  @field.field_value = 'Text'
501
580
  @field.set_default_appearance_string(font_color: "red")
@@ -10,6 +10,10 @@ describe HexaPDF::Type::AcroForm::ButtonField do
10
10
  @field = @doc.add({FT: :Btn, T: 'button'}, type: :XXAcroFormField, subtype: :Btn)
11
11
  end
12
12
 
13
+ it "identifies as an :XXAcroFormField type" do
14
+ assert_equal(:XXAcroFormField, @field.type)
15
+ end
16
+
13
17
  it "can be initialized as push button" do
14
18
  @field.initialize_as_push_button
15
19
  assert_nil(@field[:V])
@@ -232,6 +236,7 @@ describe HexaPDF::Type::AcroForm::ButtonField do
232
236
  @field.create_appearances
233
237
  yes = widget.appearance_dict.normal_appearance[:Yes]
234
238
  off = widget.appearance_dict.normal_appearance[:Off]
239
+ widget.appearance_dict.normal_appearance[:Yes] = HexaPDF::Reference.new(yes.oid)
235
240
  @field.create_appearances
236
241
  assert_same(yes, widget.appearance_dict.normal_appearance[:Yes])
237
242
  assert_same(off, widget.appearance_dict.normal_appearance[:Off])
@@ -252,7 +257,7 @@ describe HexaPDF::Type::AcroForm::ButtonField do
252
257
  refute_same(yes, widget.appearance_dict.normal_appearance[:Yes])
253
258
  end
254
259
 
255
- it "fails for unsupported button types" do
260
+ it "fails for push buttons as they are not implemented yet" do
256
261
  @field.flag(:push_button)
257
262
  @field.create_widget(@doc.pages.add, Rect: [0, 0, 0, 0])
258
263
  assert_raises(HexaPDF::Error) { @field.create_appearances }
@@ -10,6 +10,10 @@ describe HexaPDF::Type::AcroForm::ChoiceField do
10
10
  @field = @doc.add({FT: :Ch, T: 'choice'}, type: :XXAcroFormField, subtype: :Ch)
11
11
  end
12
12
 
13
+ it "identifies as an :XXAcroFormField type" do
14
+ assert_equal(:XXAcroFormField, @field.type)
15
+ end
16
+
13
17
  it "can be initialized as list box" do
14
18
  @field.initialize_as_list_box
15
19
  assert_nil(@field[:V])
@@ -111,14 +111,14 @@ describe HexaPDF::Type::AcroForm::Field do
111
111
  @field[:Subtype] = :Widget
112
112
  @field[:Rect] = [0, 0, 0, 0]
113
113
  widgets = @field.each_widget.to_a
114
- assert_kind_of(HexaPDF::Type::Annotations::Widget, *widgets)
114
+ assert_kind_of(HexaPDF::Type::Annotations::Widget, widgets.first)
115
115
  assert_same(@field.data, widgets.first.data)
116
116
  end
117
117
 
118
118
  it "yields all widgets in the /Kids array" do
119
119
  @field[:Kids] = [{Subtype: :Widget, Rect: [0, 0, 0, 0], X: 1}]
120
120
  widgets = @field.each_widget.to_a
121
- assert_kind_of(HexaPDF::Type::Annotations::Widget, *widgets)
121
+ assert_kind_of(HexaPDF::Type::Annotations::Widget, widgets.first)
122
122
  assert_equal(1, widgets.first[:X])
123
123
  end
124
124
 
@@ -128,8 +128,8 @@ describe HexaPDF::Type::AcroForm::Field do
128
128
  @doc.add({T: "b", Subtype: :Widget, Rect: [0, 0, 0, 0]}, type: :XXAcroFormField) <<
129
129
  @doc.add({T: "a", X: 1, Subtype: :Widget, Rect: [0, 0, 0, 0]}, type: :XXAcroFormField)
130
130
 
131
- widgets = @field.each_widget.to_a
132
- assert_kind_of(HexaPDF::Type::Annotations::Widget, *widgets)
131
+ widgets = @field.each_widget(direct_only: false).to_a
132
+ assert_kind_of(HexaPDF::Type::Annotations::Widget, widgets.first)
133
133
  assert_equal(1, widgets.first[:X])
134
134
  end
135
135
 
@@ -396,6 +396,24 @@ describe HexaPDF::Type::AcroForm::Form do
396
396
  end
397
397
  end
398
398
 
399
+ describe "combining fields with the same name" do
400
+ before do
401
+ @acro_form[:Fields] = [
402
+ @doc.add({T: 'e', Subtype: :Widget, Rect: [0, 0, 0, 1]}),
403
+ @doc.add({T: 'e', Subtype: :Widget, Rect: [0, 0, 0, 2]}),
404
+ @doc.add({T: 'Tx2'}),
405
+ @doc.add({T: 'e', Kids: [{Subtype: :Widget, Rect: [0, 0, 0, 3]}]}),
406
+ ]
407
+ end
408
+
409
+ it "merges fields with the same name into the first one" do
410
+ assert(@acro_form.validate)
411
+ assert_equal(2, @acro_form.root_fields.size)
412
+ assert_equal([[0, 0, 0, 1], [0, 0, 0, 2], [0, 0, 0, 3]],
413
+ @acro_form.field_by_name('e').each_widget.map {|w| w[:Rect] })
414
+ end
415
+ end
416
+
399
417
  describe "automatically creates the terminal fields; appearances" do
400
418
  before do
401
419
  @cb = @acro_form.create_check_box('test2')
@@ -20,6 +20,10 @@ describe HexaPDF::Type::AcroForm::SignatureField do
20
20
  @field = @doc.wrap({}, type: :XXAcroFormField, subtype: :Sig)
21
21
  end
22
22
 
23
+ it "identifies as an :XXAcroFormField type" do
24
+ assert_equal(:XXAcroFormField, @field.type)
25
+ end
26
+
23
27
  it "sets the field value" do
24
28
  @field.field_value = {Empty: :True}
25
29
  assert_equal({Empty: :True}, @field[:V].value)
@@ -10,6 +10,10 @@ describe HexaPDF::Type::AcroForm::TextField do
10
10
  @field = @doc.add({FT: :Tx}, type: :XXAcroFormField, subtype: :Tx)
11
11
  end
12
12
 
13
+ it "identifies as an :XXAcroFormField type" do
14
+ assert_equal(:XXAcroFormField, @field.type)
15
+ end
16
+
13
17
  it "resolves /MaxLen as inheritable field" do
14
18
  assert_nil(@field[:MaxLen])
15
19
 
@@ -164,11 +168,20 @@ describe HexaPDF::Type::AcroForm::TextField do
164
168
  assert_same(stream, @field[:AP][:N].raw_stream)
165
169
  @field.field_value = 'test'
166
170
  refute_same(stream, @field[:AP][:N].raw_stream)
171
+ stream = @field[:AP][:N].raw_stream
167
172
 
168
173
  widget = @field.create_widget(@doc.pages.add, Rect: [0, 0, 0, 0])
169
174
  assert_nil(widget[:AP])
170
175
  @field.create_appearances
171
176
  refute_nil(widget[:AP][:N])
177
+
178
+ @doc.clear_cache
179
+ @field.create_appearances
180
+ assert_same(stream, @field[:Kids][0][:AP][:N].raw_stream)
181
+
182
+ @doc.clear_cache
183
+ @field.field_value = 'other'
184
+ refute_same(stream, @field[:Kids][0][:AP][:N].raw_stream)
172
185
  end
173
186
 
174
187
  it "always creates a new appearance stream if force is true" do
@@ -85,7 +85,8 @@ module HexaPDF
85
85
  signer_cert.add_extension(extension_factory.create_extension('subjectKeyIdentifier', 'hash'))
86
86
  signer_cert.add_extension(extension_factory.create_extension('basicConstraints', 'CA:FALSE'))
87
87
  signer_cert.add_extension(extension_factory.create_extension('keyUsage', 'digitalSignature'))
88
- signer_cert.add_extension(extension_factory.create_extension('extendedKeyUsage', 'timeStamping', true))
88
+ signer_cert.add_extension(extension_factory.create_extension('extendedKeyUsage',
89
+ 'timeStamping', true))
89
90
  signer_cert.sign(ca_key, OpenSSL::Digest.new('SHA1'))
90
91
 
91
92
  signer_cert
@@ -117,6 +118,7 @@ module HexaPDF
117
118
  end
118
119
  Thread.new { @tsa_server.start }
119
120
  end
121
+
120
122
  end
121
123
 
122
124
  end
@@ -15,6 +15,26 @@ describe HexaPDF::Type::FontTrueType do
15
15
  BaseFont: :Something, FontDescriptor: font_descriptor})
16
16
  end
17
17
 
18
+ describe "font_wrapper" do
19
+ it "returns the default value if the font is subset" do
20
+ @font[:BaseFont] = :'ABCDEF+Something'
21
+ assert_nil(@font.font_wrapper)
22
+ end
23
+
24
+ it "returns the default value if the font has no embedded font file" do
25
+ assert_nil(@font.font_wrapper)
26
+ end
27
+
28
+ it "uses a fully embedded TrueType font file" do
29
+ font_file = File.binread(File.join(TEST_DATA_DIR, "fonts", "Ubuntu-Title.ttf"))
30
+ @font[:FontDescriptor][:FontFile2] = @doc.add({}, stream: font_file)
31
+ font_wrapper = @font.font_wrapper
32
+ assert(font_wrapper)
33
+ assert_equal(font_file, font_wrapper.wrapped_font.io.string)
34
+ assert_same(font_wrapper, @font.font_wrapper)
35
+ end
36
+ end
37
+
18
38
  describe "validation" do
19
39
  it "ignores some missing fields if the font name is one of the standard PDF fonts" do
20
40
  @font[:BaseFont] = :'Arial,Bold'
@@ -104,7 +104,8 @@ describe HexaPDF::Type::ObjectStream do
104
104
  assert_equal("", @obj.stream)
105
105
  end
106
106
 
107
- it "doesn't allow the Catalog entry to be compressed when encryption is used" do
107
+ it "doesn't allow the Catalog entry to be compressed" do
108
+ @doc.trailer.delete(:Encrypt)
108
109
  @obj.add_object(HexaPDF::Dictionary.new({Type: :Catalog}, oid: 8))
109
110
  @obj.write_objects(@revision)
110
111
  assert_equal(0, @obj.value[:N])
@@ -64,6 +64,9 @@ describe HexaPDF::Type::Outline do
64
64
  assert(correctable)
65
65
  end
66
66
  refute(@outline.key?(:Count))
67
+
68
+ @outline[:Count] = 0
69
+ assert(@outline.validate(auto_correct: false))
67
70
  end
68
71
  end
69
72
  end
@@ -225,29 +225,66 @@ describe HexaPDF::Type::Page do
225
225
  @page.box(:art, [0, 0, 4, 5])
226
226
 
227
227
  @page.rotate(90, flatten: true)
228
- assert_equal([-300, 50, -100, 200], @page.box(:media).value)
229
- assert_equal([-2, 0, 0, 1], @page.box(:crop).value)
230
- assert_equal([-3, 0, 0, 2], @page.box(:bleed).value)
231
- assert_equal([-4, 0, 0, 3], @page.box(:trim).value)
232
- assert_equal([-5, 0, 0, 4], @page.box(:art).value)
228
+ assert_equal([-298, 50, -98, 200], @page.box(:media).value)
229
+ assert_equal([0, 0, 2, 1], @page.box(:crop).value)
230
+ assert_equal([-1, 0, 2, 2], @page.box(:bleed).value)
231
+ assert_equal([-2, 0, 2, 3], @page.box(:trim).value)
232
+ assert_equal([-3, 0, 2, 4], @page.box(:art).value)
233
233
  end
234
234
 
235
235
  it "works correctly for 90 degrees" do
236
236
  @page.rotate(90, flatten: true)
237
- assert_equal([-300, 50, -100, 200], @page.box(:media).value)
238
- assert_equal(" q 0 1 -1 0 0 0 cm Q ", @page.contents)
237
+ assert_equal([0, 0, 200, 150], @page.box(:media).value)
238
+ assert_equal(" q 0 1 -1 0 300 -50 cm Q ", @page.contents)
239
239
  end
240
240
 
241
241
  it "works correctly for 180 degrees" do
242
242
  @page.rotate(180, flatten: true)
243
- assert_equal([-200, -300, -50, -100], @page.box(:media).value)
244
- assert_equal(" q -1 0 0 -1 0 0 cm Q ", @page.contents)
243
+ assert_equal([0, 0, 150, 200], @page.box(:media).value)
244
+ assert_equal(" q -1 0 0 -1 200 300 cm Q ", @page.contents)
245
245
  end
246
246
 
247
247
  it "works correctly for 270 degrees" do
248
248
  @page.rotate(270, flatten: true)
249
- assert_equal([100, -200, 300, -50], @page.box(:media).value)
250
- assert_equal(" q 0 -1 1 0 0 0 cm Q ", @page.contents)
249
+ assert_equal([0, 0, 200, 150], @page.box(:media).value)
250
+ assert_equal(" q 0 -1 1 0 -100 200 cm Q ", @page.contents)
251
+ end
252
+
253
+ describe "annotations" do
254
+ before do
255
+ @appearance = @doc.add({Type: :XObject, Subtype: :Form, BBox: [-10, -5, 50, 20]}, stream: "")
256
+ @annot = @doc.add({Type: :Annot, Subtype: :Widget, Rect: [100, 100, 160, 125],
257
+ QuadPoints: [0, 0, 100, 200, 300, 400, 500, 600],
258
+ AP: {N: @appearance}})
259
+ @page[:Annots] = [@annot]
260
+ end
261
+
262
+ it "rotates the /Rect entry" do
263
+ @page.rotate(90, flatten: true)
264
+ assert_equal([175, 50, 200, 110], @annot[:Rect].value)
265
+ end
266
+
267
+ it "rotates all (x,y) pairs in the /QuadPoints entry" do
268
+ @page.rotate(90, flatten: true)
269
+ assert_equal([300, -50, 100, 50, -100, 250, -300, 450],
270
+ @annot[:QuadPoints])
271
+ end
272
+
273
+ it "applies the needed matrix to the annotation's appearance stream's /Matrix entry" do
274
+ @page.rotate(90, flatten: true)
275
+ assert_equal([0, 1, -1, 0, 300, -50], @appearance[:Matrix])
276
+
277
+ @page.rotate(90, flatten: true)
278
+ assert_equal([-1, 0, 0, -1, 200, 300], @appearance[:Matrix])
279
+ end
280
+
281
+ it "modified the /R entry in the appearance characteristics dictionary of a widget annotation" do
282
+ @page.rotate(90, flatten: true)
283
+ assert_equal(90, @annot[:MK][:R])
284
+
285
+ @page.rotate(90, flatten: true)
286
+ assert_equal(180, @annot[:MK][:R])
287
+ end
251
288
  end
252
289
  end
253
290
 
@@ -494,25 +531,30 @@ describe HexaPDF::Type::Page do
494
531
  @canvas = @page.canvas(type: :overlay)
495
532
  end
496
533
 
497
- it "does nothing if the page doesn't have any annotations" do
534
+ it "does nothing and returns the argument as array if the page doesn't have any annotations" do
535
+ annots = @page[:Annots]
536
+
498
537
  @page.delete(:Annots)
499
538
  result = @page.flatten_annotations
500
539
  assert(result.empty?)
501
540
  assert_operators(@canvas.contents, [])
541
+
542
+ result = @page.flatten_annotations(annots)
543
+ assert_kind_of(Array, result)
544
+ assert_equal([@annot1, @annot2], result)
545
+ assert_operators(@canvas.contents, [])
502
546
  end
503
547
 
504
548
  it "flattens all annotations of the page by default" do
505
549
  result = @page.flatten_annotations
506
550
  assert(result.empty?)
507
551
  assert_operators(@canvas.contents, [[:save_graphics_state],
508
- [:save_graphics_state],
509
552
  [:concatenate_matrix, [1.0, 0, 0, 1.0, 110, 105]],
510
553
  [:paint_xobject, [:XO1]],
511
554
  [:restore_graphics_state],
512
555
  [:save_graphics_state],
513
556
  [:concatenate_matrix, [1.0, 0, 0, 1.0, 20, 15]],
514
557
  [:paint_xobject, [:XO1]],
515
- [:restore_graphics_state],
516
558
  [:restore_graphics_state]])
517
559
  assert(@annot1.null?)
518
560
  assert(@annot2.null?)
@@ -542,10 +584,8 @@ describe HexaPDF::Type::Page do
542
584
  assert(result.empty?)
543
585
  assert(@annot1.null?)
544
586
  assert_operators(@canvas.contents, [[:save_graphics_state],
545
- [:save_graphics_state],
546
587
  [:concatenate_matrix, [1.0, 0, 0, 1.0, 20, 15]],
547
588
  [:paint_xobject, [:XO1]],
548
- [:restore_graphics_state],
549
589
  [:restore_graphics_state]])
550
590
  end
551
591
 
@@ -555,10 +595,8 @@ describe HexaPDF::Type::Page do
555
595
  assert(result.empty?)
556
596
  assert(@annot1.null?)
557
597
  assert_operators(@canvas.contents, [[:save_graphics_state],
558
- [:save_graphics_state],
559
598
  [:concatenate_matrix, [1.0, 0, 0, 1.0, 20, 15]],
560
599
  [:paint_xobject, [:XO1]],
561
- [:restore_graphics_state],
562
600
  [:restore_graphics_state]])
563
601
  end
564
602
 
@@ -568,10 +606,8 @@ describe HexaPDF::Type::Page do
568
606
  assert_equal([@annot1], result)
569
607
  refute(@annot1.empty?)
570
608
  assert_operators(@canvas.contents, [[:save_graphics_state],
571
- [:save_graphics_state],
572
609
  [:concatenate_matrix, [1.0, 0, 0, 1.0, 20, 15]],
573
610
  [:paint_xobject, [:XO1]],
574
- [:restore_graphics_state],
575
611
  [:restore_graphics_state]])
576
612
  end
577
613
 
@@ -584,40 +620,41 @@ describe HexaPDF::Type::Page do
584
620
  it "adjusts the position in case the form /Matrix has an offset" do
585
621
  @appearance[:Matrix] = [1, 0, 0, 1, 15, 15]
586
622
  @page.flatten_annotations
587
- assert_operators(@canvas.contents, [:concatenate_matrix, [1, 0, 0, 1, 95, 90]], range: 2)
623
+ assert_operators(@canvas.contents, [:concatenate_matrix, [1, 0, 0, 1, 95, 90]], range: 1)
588
624
  end
589
625
 
590
626
  it "adjusts the position for an appearance with a 90 degree rotation" do
591
627
  @appearance[:Matrix] = [0, 1, -1, 0, 0, 0]
592
628
  @annot1[:Rect] = [100, 100, 125, 160]
593
629
  @page.flatten_annotations
594
- assert_operators(@canvas.contents, [:concatenate_matrix, [1, 0, 0, 1, 120, 110]], range: 2)
630
+ assert_operators(@canvas.contents, [:concatenate_matrix, [1, 0, 0, 1, 120, 110]], range: 1)
595
631
  end
596
632
 
597
633
  it "adjusts the position for an appearance with a -90 degree rotation" do
598
634
  @appearance[:Matrix] = [0, -1, 1, 0, 0, 0]
599
635
  @annot1[:Rect] = [100, 100, 125, 160]
600
636
  @page.flatten_annotations
601
- assert_operators(@canvas.contents, [:concatenate_matrix, [1, 0, 0, 1, 105, 150]], range: 2)
637
+ assert_operators(@canvas.contents, [:concatenate_matrix, [1, 0, 0, 1, 105, 150]], range: 1)
602
638
  end
603
639
 
604
640
  it "adjusts the position for an appearance with a 180 degree rotation" do
605
641
  @appearance[:Matrix] = [-1, 0, 0, -1, 0, 0]
606
642
  @page.flatten_annotations
607
- assert_operators(@canvas.contents, [:concatenate_matrix, [1, 0, 0, 1, 150, 120]], range: 2)
643
+ assert_operators(@canvas.contents, [:concatenate_matrix, [1, 0, 0, 1, 150, 120]], range: 1)
608
644
  end
609
645
 
610
- it "ignores an appearance with a rotation that is not a mulitple of 90" do
611
- @appearance[:Matrix] = [-1, 0.5, 0.5, -1, 0, 0]
612
- result = @page.flatten_annotations
613
- assert_equal([@annot1, @annot2], result)
614
- assert_operators(@canvas.contents, [[:save_graphics_state], [:restore_graphics_state]])
646
+ it "correctly positions and scales an appearance with a custom rotation" do
647
+ @appearance[:Matrix] = [0.707106, 0.707106, -0.707106, 0.707106, 10, 30]
648
+ @page.flatten_annotations
649
+ assert_operators(@canvas.contents,
650
+ [:concatenate_matrix, [0.998269, 0.0, 0.0, 0.415946, 111.21318, 80.60659]],
651
+ range: 1)
615
652
  end
616
653
 
617
654
  it "scales the appearance to fit into the annotations's rectangle" do
618
655
  @annot1[:Rect] = [100, 100, 130, 150]
619
656
  @page.flatten_annotations
620
- assert_operators(@canvas.contents, [:concatenate_matrix, [0.5, 0, 0, 2, 110, 105]], range: 2)
657
+ assert_operators(@canvas.contents, [:concatenate_matrix, [0.5, 0, 0, 2, 110, 105]], range: 1)
621
658
  end
622
659
  end
623
660
 
@@ -241,11 +241,13 @@ describe HexaPDF::Type::PageTreeNode do
241
241
 
242
242
  it "moves the page to the correct location within the same parent node" do
243
243
  @root.move_page(2, 4)
244
- assert_equal([@pages[0], @pages[1], @pages[3], @pages[4], @pages[2], *@pages[5..-1]], @root.each_page.to_a)
244
+ assert_equal([@pages[0], @pages[1], @pages[3], @pages[4], @pages[2], *@pages[5..-1]],
245
+ @root.each_page.to_a)
245
246
  assert(@root.validate)
246
247
 
247
248
  @root.move_page(4, 3)
248
- assert_equal([@pages[0], @pages[1], @pages[3], @pages[2], @pages[4], *@pages[5..-1]], @root.each_page.to_a)
249
+ assert_equal([@pages[0], @pages[1], @pages[3], @pages[2], @pages[4], *@pages[5..-1]],
250
+ @root.each_page.to_a)
249
251
  assert(@root.validate)
250
252
  end
251
253