hexapdf 0.33.0 → 0.34.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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +42 -1
  3. data/examples/026-optional_content.rb +55 -0
  4. data/examples/027-composer_optional_content.rb +83 -0
  5. data/lib/hexapdf/cli/command.rb +7 -1
  6. data/lib/hexapdf/cli/fonts.rb +1 -1
  7. data/lib/hexapdf/cli/inspect.rb +2 -4
  8. data/lib/hexapdf/composer.rb +2 -1
  9. data/lib/hexapdf/configuration.rb +21 -1
  10. data/lib/hexapdf/content/canvas.rb +52 -0
  11. data/lib/hexapdf/content/operator.rb +2 -0
  12. data/lib/hexapdf/dictionary.rb +1 -0
  13. data/lib/hexapdf/dictionary_fields.rb +1 -2
  14. data/lib/hexapdf/digital_signature/verification_result.rb +1 -2
  15. data/lib/hexapdf/document/layout.rb +3 -0
  16. data/lib/hexapdf/document/pages.rb +1 -1
  17. data/lib/hexapdf/document.rb +7 -0
  18. data/lib/hexapdf/encryption/ruby_aes.rb +10 -20
  19. data/lib/hexapdf/layout/box.rb +23 -3
  20. data/lib/hexapdf/layout/column_box.rb +2 -1
  21. data/lib/hexapdf/layout/frame.rb +23 -6
  22. data/lib/hexapdf/layout/inline_box.rb +20 -9
  23. data/lib/hexapdf/layout/list_box.rb +34 -20
  24. data/lib/hexapdf/layout/page_style.rb +2 -1
  25. data/lib/hexapdf/layout/style.rb +46 -6
  26. data/lib/hexapdf/layout/table_box.rb +9 -7
  27. data/lib/hexapdf/layout/text_box.rb +9 -2
  28. data/lib/hexapdf/layout/text_fragment.rb +28 -2
  29. data/lib/hexapdf/layout/text_layouter.rb +21 -5
  30. data/lib/hexapdf/stream.rb +1 -2
  31. data/lib/hexapdf/type/actions/set_ocg_state.rb +86 -0
  32. data/lib/hexapdf/type/actions.rb +1 -0
  33. data/lib/hexapdf/type/annotations/text.rb +1 -2
  34. data/lib/hexapdf/type/catalog.rb +10 -1
  35. data/lib/hexapdf/type/cid_font.rb +15 -1
  36. data/lib/hexapdf/type/form.rb +75 -5
  37. data/lib/hexapdf/type/optional_content_configuration.rb +170 -0
  38. data/lib/hexapdf/type/optional_content_group.rb +370 -0
  39. data/lib/hexapdf/type/optional_content_membership.rb +63 -0
  40. data/lib/hexapdf/type/optional_content_properties.rb +158 -0
  41. data/lib/hexapdf/type/page.rb +27 -11
  42. data/lib/hexapdf/type/page_label.rb +4 -8
  43. data/lib/hexapdf/type.rb +4 -0
  44. data/lib/hexapdf/utils/pdf_doc_encoding.rb +0 -1
  45. data/lib/hexapdf/version.rb +1 -1
  46. data/test/hexapdf/content/test_canvas.rb +49 -0
  47. data/test/hexapdf/document/test_layout.rb +7 -2
  48. data/test/hexapdf/document/test_pages.rb +6 -6
  49. data/test/hexapdf/layout/test_box.rb +13 -4
  50. data/test/hexapdf/layout/test_frame.rb +13 -1
  51. data/test/hexapdf/layout/test_inline_box.rb +17 -8
  52. data/test/hexapdf/layout/test_list_box.rb +48 -31
  53. data/test/hexapdf/layout/test_style.rb +10 -0
  54. data/test/hexapdf/layout/test_table_box.rb +32 -26
  55. data/test/hexapdf/layout/test_text_box.rb +8 -0
  56. data/test/hexapdf/layout/test_text_fragment.rb +33 -0
  57. data/test/hexapdf/layout/test_text_layouter.rb +32 -5
  58. data/test/hexapdf/test_composer.rb +10 -0
  59. data/test/hexapdf/test_dictionary.rb +10 -0
  60. data/test/hexapdf/test_document.rb +4 -0
  61. data/test/hexapdf/test_writer.rb +3 -3
  62. data/test/hexapdf/type/actions/test_set_ocg_state.rb +40 -0
  63. data/test/hexapdf/type/test_catalog.rb +11 -0
  64. data/test/hexapdf/type/test_form.rb +119 -0
  65. data/test/hexapdf/type/test_optional_content_configuration.rb +112 -0
  66. data/test/hexapdf/type/test_optional_content_group.rb +158 -0
  67. data/test/hexapdf/type/test_optional_content_properties.rb +109 -0
  68. data/test/hexapdf/type/test_page.rb +2 -2
  69. metadata +14 -3
@@ -5,6 +5,10 @@ require 'hexapdf/document'
5
5
  require 'hexapdf/layout/table_box'
6
6
 
7
7
  describe HexaPDF::Layout::TableBox::Cell do
8
+ before do
9
+ @frame = HexaPDF::Layout::Frame.new(0, 0, 0, 0)
10
+ end
11
+
8
12
  def create_cell(**kwargs)
9
13
  HexaPDF::Layout::TableBox::Cell.new(row: 1, column: 1, **kwargs)
10
14
  end
@@ -46,7 +50,7 @@ describe HexaPDF::Layout::TableBox::Cell do
46
50
 
47
51
  it "returns true if the cell has no content" do
48
52
  cell = create_cell(children: nil, style: {border: {width: 0}})
49
- cell.fit(100, 100, nil)
53
+ cell.fit(100, 100, @frame)
50
54
  assert(cell.empty?)
51
55
  end
52
56
  end
@@ -54,7 +58,7 @@ describe HexaPDF::Layout::TableBox::Cell do
54
58
  describe "update_height" do
55
59
  it "updates the height to the correct one" do
56
60
  cell = create_cell(children: HexaPDF::Layout::Box.create(width: 10, height: 10))
57
- cell.fit(100, 100, nil)
61
+ cell.fit(100, 100, @frame)
58
62
  assert_equal(22, cell.height)
59
63
  cell.update_height(50)
60
64
  assert_equal(50, cell.height)
@@ -62,7 +66,7 @@ describe HexaPDF::Layout::TableBox::Cell do
62
66
 
63
67
  it "fails if the given height is smaller than the one determined during #fit" do
64
68
  cell = create_cell(children: HexaPDF::Layout::Box.create(width: 10, height: 10))
65
- cell.fit(100, 100, nil)
69
+ cell.fit(100, 100, @frame)
66
70
  err = assert_raises(HexaPDF::Error) { cell.update_height(5) }
67
71
  assert_match(/at least as big/, err.message)
68
72
  end
@@ -71,7 +75,7 @@ describe HexaPDF::Layout::TableBox::Cell do
71
75
  describe "fit" do
72
76
  it "fits a single box" do
73
77
  cell = create_cell(children: HexaPDF::Layout::Box.create(width: 20, height: 10))
74
- cell.fit(100, 100, nil)
78
+ cell.fit(100, 100, @frame)
75
79
  assert_equal(100, cell.width)
76
80
  assert_equal(22, cell.height)
77
81
  assert_equal(32, cell.preferred_width)
@@ -80,7 +84,7 @@ describe HexaPDF::Layout::TableBox::Cell do
80
84
 
81
85
  it "fits a single box with horizontal aligning not being :left" do
82
86
  cell = create_cell(children: HexaPDF::Layout::Box.create(width: 20, height: 10, position_hint: :center))
83
- cell.fit(100, 100, nil)
87
+ cell.fit(100, 100, @frame)
84
88
  assert_equal(66, cell.preferred_width)
85
89
  end
86
90
 
@@ -88,7 +92,7 @@ describe HexaPDF::Layout::TableBox::Cell do
88
92
  box1 = HexaPDF::Layout::Box.create(width: 20, height: 10)
89
93
  box2 = HexaPDF::Layout::Box.create(width: 50, height: 15)
90
94
  cell = create_cell(children: [box1, box2])
91
- cell.fit(100, 100, nil)
95
+ cell.fit(100, 100, @frame)
92
96
  assert_equal(100, cell.width)
93
97
  assert_equal(37, cell.height)
94
98
  assert_equal(62, cell.preferred_width)
@@ -99,13 +103,13 @@ describe HexaPDF::Layout::TableBox::Cell do
99
103
  box1 = HexaPDF::Layout::Box.create(width: 20, height: 10, position_hint: :center)
100
104
  box2 = HexaPDF::Layout::Box.create(width: 50, height: 15)
101
105
  cell = create_cell(children: [box1, box2])
102
- cell.fit(100, 100, nil)
106
+ cell.fit(100, 100, @frame)
103
107
  assert_equal(66, cell.preferred_width)
104
108
  end
105
109
 
106
110
  it "fits the cell even if it has no content" do
107
111
  cell = create_cell(children: nil)
108
- cell.fit(100, 100, nil)
112
+ cell.fit(100, 100, @frame)
109
113
  assert_equal(100, cell.width)
110
114
  assert_equal(12, cell.height)
111
115
  assert_equal(12, cell.preferred_width)
@@ -114,8 +118,8 @@ describe HexaPDF::Layout::TableBox::Cell do
114
118
 
115
119
  it "doesn't fit anything if the available width or height are too small" do
116
120
  cell = create_cell(children: nil)
117
- refute(cell.fit(10, 100, nil))
118
- refute(cell.fit(100, 10, nil))
121
+ refute(cell.fit(10, 100, @frame))
122
+ refute(cell.fit(100, 10, @frame))
119
123
  end
120
124
  end
121
125
 
@@ -129,7 +133,7 @@ describe HexaPDF::Layout::TableBox::Cell do
129
133
  box1 = HexaPDF::Layout::Box.create(width: 20, height: 10, position_hint: :center, &draw_block)
130
134
  box2 = HexaPDF::Layout::Box.create(width: 50, height: 15, &draw_block)
131
135
  box = create_cell(children: [box1, box2])
132
- box.fit(100, 100, nil)
136
+ box.fit(100, 100, @frame)
133
137
  box.draw(@canvas, 10, 75)
134
138
  operators = [[:save_graphics_state],
135
139
  [:append_rectangle, [9.5, 74.5, 101.0, 38.0]],
@@ -153,7 +157,7 @@ describe HexaPDF::Layout::TableBox::Cell do
153
157
 
154
158
  it "works for a cell without content" do
155
159
  box = create_cell(children: nil, style: {border: {width: 0}})
156
- box.fit(100, 100, nil)
160
+ box.fit(100, 100, @frame)
157
161
  box.draw(@canvas, 10, 75)
158
162
  assert_operators(@canvas.contents, [])
159
163
  end
@@ -355,7 +359,9 @@ end
355
359
 
356
360
  describe HexaPDF::Layout::TableBox do
357
361
  before do
358
- @frame = HexaPDF::Layout::Frame.new(0, 0, 160, 100)
362
+ @doc = HexaPDF::Document.new
363
+ @page = @doc.pages.add
364
+ @frame = HexaPDF::Layout::Frame.new(0, 0, 160, 100, context: @page)
359
365
  draw_block = lambda {|canvas, _box| canvas.move_to(0, 0).end_path }
360
366
  @fixed_size_boxes = 15.times.map { HexaPDF::Layout::Box.new(width: 20, height: 10, &draw_block) }
361
367
  end
@@ -425,7 +431,7 @@ describe HexaPDF::Layout::TableBox do
425
431
  footer = lambda {|_| [[nil]] }
426
432
  box = create_box(header: header, footer: footer, cells: [[nil], [nil]],
427
433
  cell_style: {background_color: 'black'})
428
- refute(box.fit(100, 40, nil))
434
+ refute(box.fit(100, 40, @frame))
429
435
  box_a, box_b = box.split(100, 40, nil)
430
436
  assert_same(box_a, box)
431
437
  assert_equal('black', box_b.header_cells[0, 0].style.background_color)
@@ -544,8 +550,8 @@ describe HexaPDF::Layout::TableBox do
544
550
  describe "split" do
545
551
  it "splits the table if some rows could not be fit into the available region" do
546
552
  box = create_box
547
- refute(box.fit(100, 25, nil))
548
- box_a, box_b = box.split(100, 25, nil)
553
+ refute(box.fit(100, 25, @frame))
554
+ box_a, box_b = box.split(100, 25, @frame)
549
555
  assert_same(box_a, box)
550
556
  assert(box_b.split_box?)
551
557
 
@@ -560,8 +566,8 @@ describe HexaPDF::Layout::TableBox do
560
566
  [{header: cells_creator}, {footer: cells_creator}].each do |args|
561
567
  box = create_box(**args)
562
568
  box.cells.style(padding: 0, border: {width: 0})
563
- refute(box.fit(100, 25, nil))
564
- box_a, box_b = box.split(100, 25, nil)
569
+ refute(box.fit(100, 25, @frame))
570
+ box_a, box_b = box.split(100, 25, @frame)
565
571
  assert_nil(box_a)
566
572
  assert_same(box_b, box)
567
573
  end
@@ -571,8 +577,8 @@ describe HexaPDF::Layout::TableBox do
571
577
  cells_creator = lambda {|_| [@fixed_size_boxes[10, 2]] }
572
578
  [{header: cells_creator}, {footer: cells_creator}].each do |args|
573
579
  box = create_box(**args)
574
- refute(box.fit(100, 50, nil))
575
- box_a, box_b = box.split(100, 50, nil)
580
+ refute(box.fit(100, 50, @frame))
581
+ box_a, box_b = box.split(100, 50, @frame)
576
582
  assert_same(box_a, box)
577
583
 
578
584
  assert_equal(0, box_a.start_row_index)
@@ -591,12 +597,12 @@ describe HexaPDF::Layout::TableBox do
591
597
 
592
598
  describe "draw_content" do
593
599
  before do
594
- @canvas = HexaPDF::Document.new.pages.add.canvas
600
+ @canvas = @page.canvas
595
601
  end
596
602
 
597
603
  it "draws the result onto the canvas" do
598
604
  box = create_box
599
- box.fit(100, 100, nil)
605
+ box.fit(100, 100, @frame)
600
606
  box.draw(@canvas, 20, 10)
601
607
  operators = [[:save_graphics_state],
602
608
  [:append_rectangle, [20.0, 32.0, 50.5, 23.0]],
@@ -651,9 +657,9 @@ describe HexaPDF::Layout::TableBox do
651
657
 
652
658
  it "correctly works for split boxes" do
653
659
  box = create_box(cell_style: {padding: 0, border: {width: 0}})
654
- refute(box.fit(100, 10, nil))
655
- _, split_box = box.split(100, 10, nil)
656
- assert(split_box.fit(100, 100, nil))
660
+ refute(box.fit(100, 10, @frame))
661
+ _, split_box = box.split(100, 10, @frame)
662
+ assert(split_box.fit(100, 100, @frame))
657
663
 
658
664
  box.draw(@canvas, 20, 10)
659
665
  split_box.draw(@canvas, 0, 50)
@@ -684,7 +690,7 @@ describe HexaPDF::Layout::TableBox do
684
690
  box = create_box(header: lambda {|_| [@fixed_size_boxes[10, 1]] },
685
691
  footer: lambda {|_| [@fixed_size_boxes[12, 1]] },
686
692
  cell_style: {padding: 0, border: {width: 0}})
687
- box.fit(100, 100, nil)
693
+ box.fit(100, 100, @frame)
688
694
  box.draw(@canvas, 20, 10)
689
695
  operators = [[:save_graphics_state],
690
696
  [:concatenate_matrix, [1, 0, 0, 1, 20, 40]],
@@ -25,6 +25,14 @@ describe HexaPDF::Layout::TextBox do
25
25
  end
26
26
  end
27
27
 
28
+ it "returns the text contents as string" do
29
+ doc = HexaPDF::Document.new
30
+ font = doc.fonts.add("Times")
31
+ box = create_box([HexaPDF::Layout::TextFragment.create('Test ', font: font), @inline_box,
32
+ HexaPDF::Layout::TextFragment.create('here', font: font)])
33
+ assert_equal('Test here', box.text)
34
+ end
35
+
28
36
  describe "fit" do
29
37
  it "fits into a rectangular area" do
30
38
  box = create_box([@inline_box] * 5, style: {padding: 10})
@@ -51,6 +51,10 @@ describe HexaPDF::Layout::TextFragment do
51
51
  end
52
52
  end
53
53
 
54
+ it "returns the text value of the items as string" do
55
+ assert_equal("Hal lo\u{00a0}d\n", setup_fragment(@font.decode_utf8("Hal lo\u{00a0}d\n")).text)
56
+ end
57
+
54
58
  it "allows duplicating with only its attributes while also setting new items" do
55
59
  setup_fragment([20])
56
60
  @fragment.properties['key'] = :value
@@ -392,6 +396,35 @@ describe HexaPDF::Layout::TextFragment do
392
396
  end
393
397
  end
394
398
 
399
+ describe "fill_horizontal!" do
400
+ before do
401
+ @fragment = HexaPDF::Layout::TextFragment.create('ab', fill_horizontal: 1, font: @font)
402
+ end
403
+
404
+ it "returns the fragment if the given width is too small" do
405
+ assert_same(@fragment, @fragment.fill_horizontal!(0.1))
406
+ end
407
+
408
+ it "repeats all items of the fragment" do
409
+ fragment = @fragment.fill_horizontal!(@fragment.width * 2)
410
+ assert_equal([*(@fragment.items * 2), 0], fragment.items)
411
+ assert_in_delta(@fragment.width * 2, fragment.width)
412
+ end
413
+
414
+ it "adds, after repeating, items from the start of the fragment to fill the available space" do
415
+ fragment = @fragment.fill_horizontal!(90)
416
+ assert_equal([*(@fragment.items * 9), @fragment.items[0], 3.3333333333332673], fragment.items)
417
+ assert_in_delta(90, fragment.width)
418
+ end
419
+
420
+ it "sets the character spacing correctly to account for the remaining space after filling with items" do
421
+ fragment = @fragment.fill_horizontal!(90)
422
+ refute_same(@fragment.style, fragment.style)
423
+ assert_equal(0.033333333333332674, fragment.style.character_spacing)
424
+ assert_in_delta(90, fragment.width)
425
+ end
426
+ end
427
+
395
428
  it "can be inspected" do
396
429
  frag = setup_fragment(@font.decode_utf8("H") << 5)
397
430
  assert_match(/:H/, frag.inspect)
@@ -262,7 +262,7 @@ module CommonLineWrappingTests
262
262
  end
263
263
 
264
264
  it "handles prohibited breakpoint penalties with non-zero width" do
265
- item = boxes(20).first
265
+ item = boxes(20).first.item
266
266
  result = call(boxes(70) + [glue(10)] + boxes(10) + [penalty(5000, item)] + boxes(30))
267
267
  assert_line_wrapping(result, [70, 60])
268
268
  end
@@ -295,11 +295,34 @@ module CommonLineWrappingTests
295
295
  assert_equal(2, lines.count)
296
296
  end
297
297
 
298
+ it "handles items with fill_horizontal correctly" do
299
+ doc = HexaPDF::Document.new
300
+ font = doc.fonts.add("Times")
301
+ box1 = HexaPDF::Layout::TextLayouter::Box.new(
302
+ HexaPDF::Layout::TextFragment.create('.', font: font, fill_horizontal: 1)
303
+ )
304
+ box2 = HexaPDF::Layout::TextLayouter::Box.new(
305
+ HexaPDF::Layout::TextFragment.create('.', font: font, fill_horizontal: 2)
306
+ )
307
+ items = [box1, *boxes(10), box2]
308
+ rest, lines = call(items, 40)
309
+ assert_equal(0, rest.size)
310
+ assert_equal(1, lines.size)
311
+
312
+ line = lines.first
313
+ refute_same(items[0].item, line.items[0])
314
+ assert_same(items[1].item, line.items[1])
315
+ refute_same(items[2].item, line.items[2])
316
+ assert_equal(10, line.items[0].width)
317
+ assert_equal(10, line.items[1].width)
318
+ assert_equal(20, line.items[2].width)
319
+ end
298
320
  end
299
321
 
300
322
  describe HexaPDF::Layout::TextLayouter::SimpleLineWrapping do
301
323
  before do
302
324
  @obj = HexaPDF::Layout::TextLayouter::SimpleLineWrapping
325
+ @mock_frame = nil
303
326
  end
304
327
 
305
328
  describe "fixed width wrapping" do
@@ -308,7 +331,9 @@ describe HexaPDF::Layout::TextLayouter::SimpleLineWrapping do
308
331
  def call(items, width = 100, &block)
309
332
  lines = []
310
333
  block ||= proc { true }
311
- rest = @obj.call(items, proc { width }) {|line, item| lines << line; block.call(line, item) }
334
+ rest = @obj.call(items, proc { width }, @mock_frame) do |line, item|
335
+ lines << line; block.call(line, item)
336
+ end
312
337
  [rest, lines]
313
338
  end
314
339
  end
@@ -319,7 +344,9 @@ describe HexaPDF::Layout::TextLayouter::SimpleLineWrapping do
319
344
  def call(items, width = 100, &block)
320
345
  lines = []
321
346
  block ||= proc { true }
322
- rest = @obj.call(items, proc {|_| width }) {|line, i| lines << line; block.call(line, i) }
347
+ rest = @obj.call(items, proc {|_| width }, @mock_frame) do |line, i|
348
+ lines << line; block.call(line, i)
349
+ end
323
350
  [rest, lines]
324
351
  end
325
352
 
@@ -334,7 +361,7 @@ describe HexaPDF::Layout::TextLayouter::SimpleLineWrapping do
334
361
  end
335
362
  end
336
363
  lines = []
337
- rest = @obj.call(boxes([20, 10], [10, 10], [20, 15], [40, 10]), width_block) do |line|
364
+ rest = @obj.call(boxes([20, 10], [10, 10], [20, 15], [40, 10]), width_block, @mock_frame) do |line|
338
365
  height += line.height
339
366
  lines << line
340
367
  true
@@ -357,7 +384,7 @@ describe HexaPDF::Layout::TextLayouter::SimpleLineWrapping do
357
384
  lines = []
358
385
  item = HexaPDF::Layout::InlineBox.create(width: 20, height: 10) {}
359
386
  items = boxes([20, 10]) + [penalty(0, item)] + boxes([40, 15])
360
- rest = @obj.call(items, width_block) do |line|
387
+ rest = @obj.call(items, width_block, @mock_frame) do |line|
361
388
  height += line.height
362
389
  lines << line
363
390
  true
@@ -243,6 +243,16 @@ describe HexaPDF::Composer do
243
243
  [:restore_graphics_state]])
244
244
  end
245
245
 
246
+ it "returns the last drawn box" do
247
+ box = create_box(height: 400)
248
+ assert_same(box, @composer.draw_box(box))
249
+
250
+ box = create_box(height: 400)
251
+ split_box = create_box(height: 100)
252
+ box.define_singleton_method(:split) {|*| [box, split_box] }
253
+ assert_same(split_box, @composer.draw_box(box))
254
+ end
255
+
246
256
  it "raises an error if a box doesn't fit onto an empty page" do
247
257
  assert_raises(HexaPDF::Error) do
248
258
  @composer.draw_box(create_box(height: 800))
@@ -94,6 +94,16 @@ describe HexaPDF::Dictionary do
94
94
  obj = @test_class.new(nil)
95
95
  assert_equal(:MyType, obj.value[:Type])
96
96
  end
97
+
98
+ it "doesn't set the default values for required fields if the type class might be wrong" do
99
+ @test_class.define_type(:MyType)
100
+ obj = @test_class.new({})
101
+ assert_equal([], obj.value[:Array])
102
+ obj = @test_class.new({Type: :MyType})
103
+ assert_equal([], obj.value[:Array])
104
+ obj = @test_class.new({Type: :OtherType})
105
+ refute(obj.key?(:Array))
106
+ end
97
107
  end
98
108
 
99
109
  describe "[]" do
@@ -561,6 +561,10 @@ describe HexaPDF::Document do
561
561
  assert_kind_of(HexaPDF::Type::Outline, @doc.outline)
562
562
  end
563
563
 
564
+ it "returns the optional content properties" do
565
+ assert_kind_of(HexaPDF::Type::OptionalContentProperties, @doc.optional_content)
566
+ end
567
+
564
568
  it "can be inspected and the output is not too large" do
565
569
  assert_match(/HexaPDF::Document:\d+/, @doc.inspect)
566
570
  end
@@ -40,7 +40,7 @@ describe HexaPDF::Writer do
40
40
  219
41
41
  %%EOF
42
42
  3 0 obj
43
- <</Producer(HexaPDF version 0.33.0)>>
43
+ <</Producer(HexaPDF version 0.34.0)>>
44
44
  endobj
45
45
  xref
46
46
  3 1
@@ -72,7 +72,7 @@ describe HexaPDF::Writer do
72
72
  141
73
73
  %%EOF
74
74
  6 0 obj
75
- <</Producer(HexaPDF version 0.33.0)>>
75
+ <</Producer(HexaPDF version 0.34.0)>>
76
76
  endobj
77
77
  2 0 obj
78
78
  <</Length 10>>stream
@@ -214,7 +214,7 @@ describe HexaPDF::Writer do
214
214
  <</Type/Page/MediaBox[0 0 595 842]/Parent 2 0 R/Resources<<>>>>
215
215
  endobj
216
216
  5 0 obj
217
- <</Producer(HexaPDF version 0.33.0)>>
217
+ <</Producer(HexaPDF version 0.34.0)>>
218
218
  endobj
219
219
  4 0 obj
220
220
  <</Root 1 0 R/Info 5 0 R/Size 6/Type/XRef/W[1 1 2]/Index[0 6]/Filter/FlateDecode/DecodeParms<</Columns 4/Predictor 12>>/Length 33>>stream
@@ -0,0 +1,40 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'test_helper'
4
+ require 'hexapdf/document'
5
+ require 'hexapdf/type/actions/set_ocg_state'
6
+
7
+ describe HexaPDF::Type::Actions::SetOCGState do
8
+ before do
9
+ @doc = HexaPDF::Document.new
10
+ @action = HexaPDF::Type::Actions::SetOCGState.new({}, document: @doc)
11
+ @ocg = @doc.optional_content.add_ocg('Test')
12
+ end
13
+
14
+ describe "add_state_change" do
15
+ it "allows using Ruby-esque and PDF type names for the state change type" do
16
+ @action.add_state_change(:on, @ocg)
17
+ @action.add_state_change(:ON, @ocg)
18
+ @action.add_state_change(:off, @ocg)
19
+ @action.add_state_change(:OFF, @ocg)
20
+ @action.add_state_change(:toggle, @ocg)
21
+ @action.add_state_change(:Toggle, @ocg)
22
+ assert_equal([:ON, @ocg, :ON, @ocg, :OFF, @ocg, :OFF, @ocg, :Toggle, @ocg, :Toggle, @ocg],
23
+ @action[:State].value)
24
+ end
25
+
26
+ it "allows specifying more than one OCG" do
27
+ @action.add_state_change(:on, [@ocg, @doc.optional_content.add_ocg('Test2')])
28
+ assert_equal([:ON, @ocg, @doc.optional_content.ocg('Test2')], @action[:State].value)
29
+ end
30
+
31
+ it "raises an error if the provide state change type is invalid" do
32
+ assert_raises(ArgumentError) { @action.add_state_change(:unknown, nil) }
33
+ end
34
+
35
+ it "raises an error if an OCG specified via a string does not exist" do
36
+ error = assert_raises(HexaPDF::Error) { @action.add_state_change(:on, "Unknown") }
37
+ assert_match(/Invalid OCG.*Unknown.*specified/, error.message)
38
+ end
39
+ end
40
+ end
@@ -39,6 +39,17 @@ describe HexaPDF::Type::Catalog do
39
39
  assert_same(outline, @catalog.outline)
40
40
  end
41
41
 
42
+ it "uses or creates the optional content properties dictionary on access" do
43
+ @catalog[:OCProperties] = hash = {}
44
+ assert_equal(:XXOCProperties, @catalog.optional_content.type)
45
+ assert_same(hash, @catalog.optional_content.value)
46
+
47
+ @catalog.delete(:OCProperties)
48
+ oc = @catalog.optional_content
49
+ assert_equal([], oc[:OCGs])
50
+ assert_equal(:XXOCConfiguration, oc[:D].type)
51
+ end
52
+
42
53
  describe "acro_form" do
43
54
  it "returns an existing form object" do
44
55
  @catalog[:AcroForm] = :test
@@ -1,6 +1,8 @@
1
1
  # -*- encoding: utf-8 -*-
2
2
 
3
3
  require 'test_helper'
4
+ require 'stringio'
5
+ require 'tempfile'
4
6
  require 'hexapdf/document'
5
7
  require 'hexapdf/type/form'
6
8
 
@@ -85,6 +87,18 @@ describe HexaPDF::Type::Form do
85
87
  @form.process_contents(processor, original_resources: resources)
86
88
  assert_same(resources, processor.resources)
87
89
  end
90
+
91
+ it "uses the referenced content in case of a Reference XObject" do
92
+ @form[:Ref] = @doc.add({F: {}})
93
+ io = StringIO.new
94
+ HexaPDF::Document.new.tap {|d| d.pages.add.canvas.line_width(5) }.write(io)
95
+ @form[:Ref][:F].embed(io, name: 'test')
96
+ @form[:Ref][:Page] = 0
97
+
98
+ processor = HexaPDF::TestUtils::OperatorRecorder.new
99
+ @form.process_contents(processor)
100
+ assert_equal([[:set_line_width, [5]]], processor.recorded_ops)
101
+ end
88
102
  end
89
103
 
90
104
  describe "canvas" do
@@ -116,4 +130,109 @@ describe HexaPDF::Type::Form do
116
130
  assert_raises(HexaPDF::Error) { @form.canvas }
117
131
  end
118
132
  end
133
+
134
+ describe "reference_xobject?" do
135
+ it "returns true if the form is a reference XObject" do
136
+ refute(@form.reference_xobject?)
137
+ @form[:Ref] = {}
138
+ assert(@form.reference_xobject?)
139
+ end
140
+ end
141
+
142
+ describe "referenced_content" do
143
+ before do
144
+ @form[:BBox] = [10, 10, 110, 60]
145
+ @form[:Matrix] = [1, 0, 0, 1, 10, 20]
146
+ @form[:Ref] = @doc.add({F: {}})
147
+ @ref = @form[:Ref]
148
+ end
149
+
150
+ it "returns a Form XObject with the imported page from an embedded file" do
151
+ io = StringIO.new
152
+ HexaPDF::Document.new.tap {|d| d.pages.add.canvas.line_width(5) }.write(io)
153
+ @ref[:F].embed(io, name: 'test.pdf')
154
+ @ref[:Page] = 0
155
+
156
+ ref_form = @form.referenced_content
157
+ refute_nil(ref_form)
158
+ assert_equal([10, 10, 110, 60], ref_form[:BBox].value)
159
+ assert_equal([1, 0, 0, 1, 10, 20], ref_form[:Matrix].value)
160
+ assert_equal("5 w\n", ref_form.contents)
161
+ end
162
+
163
+ it "returns a Form XObject with the imported page from an external file" do
164
+ file = Tempfile.new('hexapdf')
165
+ HexaPDF::Document.new.tap {|d| d.pages.add.canvas.line_width(5) }.write(file.path)
166
+ @ref[:F].path = file.path
167
+ @ref[:Page] = 0
168
+ assert_equal("5 w\n", @form.referenced_content.contents)
169
+ end
170
+
171
+ it "also works with a page label" do
172
+ file = Tempfile.new('hexapdf')
173
+ HexaPDF::Document.new.tap do |d|
174
+ d.pages.add
175
+ d.pages.add
176
+ d.pages.add.canvas.line_width(5)
177
+ d.pages.add
178
+ d.pages.add_labelling_range(1, numbering_style: :decimal, prefix: 'Test', start_number: 4)
179
+ end.write(file.path)
180
+ @ref[:F].path = file.path
181
+ @ref[:Page] = 'Test5'
182
+ assert_equal("5 w\n", @form.referenced_content.contents)
183
+ end
184
+
185
+ it "flattens printable annotations into the page's content stream" do
186
+ io = StringIO.new
187
+ HexaPDF::Document.new.tap do |d|
188
+ d.pages.add.canvas.line_width(5)
189
+ tf = d.acro_form(create: true).create_text_field('text')
190
+ widget = tf.create_widget(d.pages[0], Rect: [10, 10, 30, 30])
191
+ widget.border_style(color: "black")
192
+ widget = tf.create_widget(d.pages[0], Rect: [40, 10, 70, 30])
193
+ widget.border_style(color: "red")
194
+ tf.field_value = 't'
195
+ widget.unflag(:print)
196
+ end.write(io)
197
+ @ref[:F].embed(io, name: 'Test')
198
+ @ref[:Page] = 0
199
+ assert_equal(" q Q q 5 w\n Q q q\n1.0 0 0 1.0 10.0 10.0 cm\n/XO1 Do\nQ\n Q ",
200
+ @form.referenced_content.contents)
201
+ end
202
+
203
+ it "returns nil if the form is not a reference XObject" do
204
+ @form.delete(:Ref)
205
+ assert_nil(@form.referenced_content)
206
+ end
207
+
208
+ it "returns nil if the file is not embedded and not found" do
209
+ @ref[:F].path = '/tmp/non_existing_path'
210
+ assert_nil(@form.referenced_content)
211
+ end
212
+
213
+ it "returns nil if the page referenced by page number is not found" do
214
+ io = StringIO.new
215
+ HexaPDF::Document.new.tap {|d| d.pages.add; d.pages.add }.write(io)
216
+ @ref[:F].embed(io, name: 'test.pdf')
217
+ @ref[:Page] = 5
218
+ assert_nil(@form.referenced_content)
219
+ end
220
+
221
+ it "returns nil if the page referenced by page label is not found" do
222
+ io = StringIO.new
223
+ HexaPDF::Document.new.tap do |d|
224
+ d.pages.add
225
+ d.pages.add
226
+ d.pages.add_labelling_range(1, numbering_style: :decimal, prefix: 'Test')
227
+ end.write(io)
228
+ @ref[:F].embed(io, name: 'test.pdf')
229
+ @ref[:Page] = 'Test5'
230
+ assert_nil(@form.referenced_content)
231
+ end
232
+
233
+ it "returns nil if an error happens during processing" do
234
+ @ref[:F].embed(StringIO.new('temp'), name: 'test.pdf')
235
+ assert_nil(@form.referenced_content)
236
+ end
237
+ end
119
238
  end