hexapdf 1.2.0 → 1.4.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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +90 -0
  3. data/README.md +1 -1
  4. data/lib/hexapdf/cli/form.rb +9 -4
  5. data/lib/hexapdf/cli/inspect.rb +13 -4
  6. data/lib/hexapdf/composer.rb +14 -0
  7. data/lib/hexapdf/configuration.rb +15 -0
  8. data/lib/hexapdf/dictionary_fields.rb +1 -1
  9. data/lib/hexapdf/digital_signature/signing/default_handler.rb +1 -2
  10. data/lib/hexapdf/document/annotations.rb +107 -2
  11. data/lib/hexapdf/document/layout.rb +94 -15
  12. data/lib/hexapdf/document/metadata.rb +10 -3
  13. data/lib/hexapdf/document.rb +9 -0
  14. data/lib/hexapdf/encryption/standard_security_handler.rb +7 -2
  15. data/lib/hexapdf/error.rb +11 -3
  16. data/lib/hexapdf/font/true_type/subsetter.rb +15 -2
  17. data/lib/hexapdf/layout/box.rb +5 -0
  18. data/lib/hexapdf/layout/container_box.rb +63 -28
  19. data/lib/hexapdf/layout/style.rb +129 -20
  20. data/lib/hexapdf/layout/table_box.rb +20 -2
  21. data/lib/hexapdf/object.rb +2 -2
  22. data/lib/hexapdf/pdf_array.rb +25 -3
  23. data/lib/hexapdf/tokenizer.rb +4 -1
  24. data/lib/hexapdf/type/acro_form/appearance_generator.rb +57 -8
  25. data/lib/hexapdf/type/acro_form/field.rb +1 -0
  26. data/lib/hexapdf/type/acro_form/form.rb +7 -6
  27. data/lib/hexapdf/type/annotation.rb +12 -0
  28. data/lib/hexapdf/type/annotations/appearance_generator.rb +169 -16
  29. data/lib/hexapdf/type/annotations/border_effect.rb +99 -0
  30. data/lib/hexapdf/type/annotations/circle.rb +65 -0
  31. data/lib/hexapdf/type/annotations/interior_color.rb +84 -0
  32. data/lib/hexapdf/type/annotations/line.rb +5 -192
  33. data/lib/hexapdf/type/annotations/line_ending_styling.rb +208 -0
  34. data/lib/hexapdf/type/annotations/markup_annotation.rb +0 -1
  35. data/lib/hexapdf/type/annotations/polygon.rb +64 -0
  36. data/lib/hexapdf/type/annotations/polygon_polyline.rb +109 -0
  37. data/lib/hexapdf/type/annotations/polyline.rb +64 -0
  38. data/lib/hexapdf/type/annotations/square.rb +65 -0
  39. data/lib/hexapdf/type/annotations/square_circle.rb +77 -0
  40. data/lib/hexapdf/type/annotations/widget.rb +50 -20
  41. data/lib/hexapdf/type/annotations.rb +9 -0
  42. data/lib/hexapdf/type/measure.rb +57 -0
  43. data/lib/hexapdf/type.rb +1 -0
  44. data/lib/hexapdf/version.rb +1 -1
  45. data/test/hexapdf/digital_signature/signing/test_default_handler.rb +0 -1
  46. data/test/hexapdf/document/test_annotations.rb +42 -0
  47. data/test/hexapdf/document/test_layout.rb +38 -10
  48. data/test/hexapdf/document/test_metadata.rb +13 -1
  49. data/test/hexapdf/encryption/test_standard_security_handler.rb +2 -1
  50. data/test/hexapdf/font/true_type/test_subsetter.rb +10 -0
  51. data/test/hexapdf/layout/test_box.rb +8 -0
  52. data/test/hexapdf/layout/test_container_box.rb +34 -6
  53. data/test/hexapdf/layout/test_page_style.rb +1 -1
  54. data/test/hexapdf/layout/test_style.rb +46 -2
  55. data/test/hexapdf/layout/test_table_box.rb +14 -1
  56. data/test/hexapdf/test_composer.rb +7 -0
  57. data/test/hexapdf/test_dictionary_fields.rb +1 -0
  58. data/test/hexapdf/test_object.rb +1 -1
  59. data/test/hexapdf/test_pdf_array.rb +36 -3
  60. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +78 -3
  61. data/test/hexapdf/type/acro_form/test_button_field.rb +7 -6
  62. data/test/hexapdf/type/acro_form/test_field.rb +5 -0
  63. data/test/hexapdf/type/acro_form/test_form.rb +17 -1
  64. data/test/hexapdf/type/annotations/test_appearance_generator.rb +210 -0
  65. data/test/hexapdf/type/annotations/test_border_effect.rb +59 -0
  66. data/test/hexapdf/type/annotations/test_interior_color.rb +37 -0
  67. data/test/hexapdf/type/annotations/test_line.rb +0 -45
  68. data/test/hexapdf/type/annotations/test_line_ending_styling.rb +42 -0
  69. data/test/hexapdf/type/annotations/test_polygon_polyline.rb +29 -0
  70. data/test/hexapdf/type/annotations/test_widget.rb +35 -0
  71. metadata +16 -2
@@ -10,6 +10,10 @@ describe HexaPDF::Layout::ContainerBox do
10
10
  @frame = HexaPDF::Layout::Frame.new(0, 0, 100, 100)
11
11
  end
12
12
 
13
+ def child_box(height: 0, width: 0, **style_properties)
14
+ @doc.layout.box(height: height, width: width, style: style_properties)
15
+ end
16
+
13
17
  def create_box(children, **kwargs)
14
18
  HexaPDF::Layout::ContainerBox.new(children: Array(children), **kwargs)
15
19
  end
@@ -48,17 +52,41 @@ describe HexaPDF::Layout::ContainerBox do
48
52
 
49
53
  describe "fit_content" do
50
54
  it "fits the children according to their mask_mode, valign, and align style properties" do
51
- box = create_box([@doc.layout.box(height: 20),
52
- @doc.layout.box(height: 20, style: {valign: :bottom, mask_mode: :fill_horizontal}),
53
- @doc.layout.box(width: 20, style: {align: :right, mask_mode: :fill_vertical})])
55
+ box = create_box([child_box(height: 20),
56
+ child_box(height: 20, valign: :bottom, mask_mode: :fill_horizontal),
57
+ child_box(width: 20, align: :right, mask_mode: :fill_vertical)])
54
58
  check_box(box, 100, 100, [[0, 80], [0, 0], [80, 20]])
55
59
  end
56
60
 
57
61
  it "respects the initially set width/height as well as border/padding" do
58
- box = create_box(@doc.layout.box(height: 20), height: 50, width: 40,
62
+ box = create_box(child_box(height: 20), height: 50, width: 40,
59
63
  style: {padding: 2, border: {width: 3}})
60
64
  check_box(box, 40, 50, [[5, 75]])
61
65
  end
66
+
67
+ it "fails if splitting is not allowed and the content is too big" do
68
+ box = create_box([child_box(height: 80), child_box(height: 30)])
69
+ box.fit(@frame.available_width, @frame.available_height, @frame)
70
+ assert(box.fit_result.failure?)
71
+ end
72
+
73
+ it "splits the box if splitting is allowed and the content is too big" do
74
+ box = create_box([child_box(height: 80), child_box(height: 30)], splitable: true)
75
+ box.fit(@frame.available_width, @frame.available_height, @frame)
76
+ assert(box.fit_result.overflow?)
77
+ end
78
+ end
79
+
80
+ describe "split_content" do
81
+ it "assigns the overflown boxes to the split box" do
82
+ box = create_box([child_box(height: 80), child_box(height: 30)], splitable: true)
83
+ box.fit(@frame.available_width, @frame.available_height, @frame)
84
+ assert(box.fit_result.overflow?)
85
+ box_a, box_b = box.split
86
+ assert_same(box, box_a)
87
+ assert(box_b.split_box?)
88
+ assert_equal(1, box_b.children.size)
89
+ end
62
90
  end
63
91
 
64
92
  describe "draw_content" do
@@ -67,10 +95,10 @@ describe HexaPDF::Layout::ContainerBox do
67
95
  end
68
96
 
69
97
  it "draws the result onto the canvas" do
70
- child_box = @doc.layout.box(height: 20) do |canvas, b|
98
+ cbox = @doc.layout.box(height: 20) do |canvas, b|
71
99
  canvas.line(0, 0, b.content_width, b.content_height).end_path
72
100
  end
73
- box = create_box(child_box)
101
+ box = create_box(cbox)
74
102
  box.fit(100, 100, @frame)
75
103
  box.draw(@canvas, 0, 50)
76
104
  assert_operators(@canvas.contents, [[:save_graphics_state],
@@ -49,7 +49,7 @@ describe HexaPDF::Layout::PageStyle do
49
49
  assert_equal("", page1.contents)
50
50
  assert_equal(523.275591, style.frame.width)
51
51
 
52
- page2 = style.create_page(@doc)
52
+ style.create_page(@doc)
53
53
  refute_same(frame1, style.frame)
54
54
  end
55
55
 
@@ -136,6 +136,19 @@ describe HexaPDF::Layout::Style::Quad do
136
136
  assert_equal(new_quad.bottom, quad.bottom)
137
137
  assert_equal(new_quad.left, quad.left)
138
138
  end
139
+
140
+ it "works with a Hash as value" do
141
+ quad = create_quad(top: 5, left: 10)
142
+ assert_equal(5, quad.top)
143
+ assert_equal(0, quad.bottom)
144
+ assert_equal(10, quad.left)
145
+ assert_equal(0, quad.right)
146
+ quad.set(right: 7)
147
+ assert_equal(5, quad.top)
148
+ assert_equal(0, quad.bottom)
149
+ assert_equal(10, quad.left)
150
+ assert_equal(7, quad.right)
151
+ end
139
152
  end
140
153
 
141
154
  it "can be asked if it contains only a single value" do
@@ -738,6 +751,30 @@ describe HexaPDF::Layout::Style do
738
751
  end
739
752
  end
740
753
 
754
+ describe "each_property" do
755
+ it "yields all set properties with their values" do
756
+ @style.font_size = 5
757
+ @style.line_spacing = 1.2
758
+ assert_equal(0.005, @style.scaled_font_size)
759
+ assert_equal([[:font, @style.font], [:font_size, 5], [:horizontal_scaling, 100],
760
+ [:line_spacing, @style.line_spacing]],
761
+ @style.each_property.to_a.sort)
762
+ end
763
+ end
764
+
765
+ describe "merge" do
766
+ it "merges all set properties" do
767
+ @style.font_size = 5
768
+ @style.line_spacing = 1.2
769
+ new_style = HexaPDF::Layout::Style.new
770
+ new_style.update(font_size: 3, line_spacing: {type: :fixed, value: 2.5})
771
+ new_style.merge(@style)
772
+ assert_equal(5, new_style.font_size)
773
+ assert_equal(:proportional, new_style.line_spacing.type)
774
+ assert_equal(1.2, new_style.line_spacing.value)
775
+ end
776
+ end
777
+
741
778
  it "has several simple and dynamically generated properties with default values" do
742
779
  @style = HexaPDF::Layout::Style.new
743
780
  assert_raises(HexaPDF::Error) { @style.font }
@@ -780,6 +817,7 @@ describe HexaPDF::Layout::Style do
780
817
  assert_equal(:left, @style.align)
781
818
  assert_equal(:top, @style.valign)
782
819
  assert_equal(:default, @style.mask_mode)
820
+ assert_equal({}, @style.box_options)
783
821
  end
784
822
 
785
823
  it "allows using a non-standard setter for generated properties" do
@@ -790,8 +828,8 @@ describe HexaPDF::Layout::Style do
790
828
  @style.stroke_dash_pattern(5, 2)
791
829
  assert_equal([[5], 2], @style.stroke_dash_pattern.to_operands)
792
830
 
793
- @style.line_spacing(1.2)
794
- assert_equal([:proportional, 1.2], [@style.line_spacing.type, @style.line_spacing.value])
831
+ @style.line_spacing(HexaPDF::Layout::Style::LineSpacing.new(type: :double))
832
+ assert_equal([:proportional, 2], [@style.line_spacing.type, @style.line_spacing.value])
795
833
  end
796
834
 
797
835
  it "allows checking for valid values" do
@@ -862,6 +900,9 @@ describe HexaPDF::Layout::Style do
862
900
  end
863
901
 
864
902
  it "handles subscript" do
903
+ @style.subscript = false
904
+ assert_equal(10, @style.calculated_font_size)
905
+ assert_equal(0, @style.calculated_text_rise)
865
906
  @style.subscript = true
866
907
  assert_in_delta(5.83, @style.calculated_font_size)
867
908
  assert_in_delta(0.00583, @style.scaled_font_size, 0.000001)
@@ -869,6 +910,9 @@ describe HexaPDF::Layout::Style do
869
910
  end
870
911
 
871
912
  it "handles superscript" do
913
+ @style.superscript = false
914
+ assert_equal(10, @style.calculated_font_size)
915
+ assert_equal(0, @style.calculated_text_rise)
872
916
  @style.superscript = true
873
917
  assert_in_delta(5.83, @style.calculated_font_size)
874
918
  assert_in_delta(3.30, @style.calculated_text_rise)
@@ -116,6 +116,18 @@ describe HexaPDF::Layout::TableBox::Cell do
116
116
  assert_equal(12, cell.preferred_height)
117
117
  end
118
118
 
119
+ it "respects the set minimum height of the cell" do
120
+ cell = create_cell(children: HexaPDF::Layout::Box.create(width: 20, height: 10), min_height: 30)
121
+ assert(cell.fit(100, 25, @frame).failure?)
122
+
123
+ assert(cell.fit(100, 100, @frame).success?)
124
+ assert_equal(30, cell.height)
125
+
126
+ cell = create_cell(children: HexaPDF::Layout::Box.create(width: 20, height: 20), min_height: 2)
127
+ assert(cell.fit(100, 100, @frame).success?)
128
+ assert_equal(32, cell.height)
129
+ end
130
+
119
131
  it "doesn't fit children that are too big" do
120
132
  cell = create_cell(children: HexaPDF::Layout::Box.create(width: 300, height: 20))
121
133
  assert(cell.fit(100, 100, @frame).failure?)
@@ -262,7 +274,7 @@ describe HexaPDF::Layout::TableBox::Cells do
262
274
  end
263
275
 
264
276
  it "sets the correct information on the created cells" do
265
- cells = create_cells([[:a, {col_span: 2, content: :b}],
277
+ cells = create_cells([[:a, {col_span: 2, content: :b, min_height: 30}],
266
278
  [{col_span: 2, row_span: 2, content: :c}, {row_span: 2, content: :d}]])
267
279
  assert_equal(0, cells[0, 0].row)
268
280
  assert_equal(0, cells[0, 0].column)
@@ -272,6 +284,7 @@ describe HexaPDF::Layout::TableBox::Cells do
272
284
  assert_equal(1, cells[0, 1].column)
273
285
  assert_equal(1, cells[0, 1].row_span)
274
286
  assert_equal(2, cells[0, 1].col_span)
287
+ assert_equal(30, cells[0, 1].instance_variable_get(:@min_height))
275
288
  assert_equal(1, cells[1, 0].row)
276
289
  assert_equal(0, cells[1, 0].column)
277
290
  assert_equal(2, cells[1, 0].row_span)
@@ -127,6 +127,13 @@ describe HexaPDF::Composer do
127
127
  end
128
128
  end
129
129
 
130
+ describe "style?" do
131
+ it "delegates to layout.style?" do
132
+ @composer.document.layout.style(:header, font_size: 20)
133
+ assert(@composer.style?(:header))
134
+ end
135
+ end
136
+
130
137
  describe "styles" do
131
138
  it "delegates to layout.styles" do
132
139
  @composer.styles(base: {font_size: 30}, other: {font_size: 40})
@@ -193,6 +193,7 @@ describe HexaPDF::DictionaryFields do
193
193
  ["D:19981223195210Z00'00", [1998, 12, 23, 19, 52, 10, "+00:00"]],
194
194
  ["D:19981223195210-08", [1998, 12, 23, 19, 52, 10, "-08:00"]], # missing '
195
195
  ["D:19981223195210-08'00", [1998, 12, 23, 19, 52, 10, "-08:00"]], # no trailing ', as per PDF 2.0
196
+ ["D:19981223195210-08'00''", [1998, 12, 23, 19, 52, 10, "-08:00"]], # two trailing '
196
197
  ["D:19981223195210-54'00", [1998, 12, 23, 19, 52, 10, "-23:59:59"]], # TZ hour too large
197
198
  ["D:19981223195210+10'65", [1998, 12, 23, 19, 52, 10, "+11:05"]], # TZ min too large
198
199
  ["D:19982423195210-08'00'", [1998, 12, 23, 19, 52, 10, "-08:00"]], # months too large
@@ -197,7 +197,7 @@ describe HexaPDF::Object do
197
197
  @obj.define_singleton_method(:perform_validation) { raise "Unknown" }
198
198
  invoked = []
199
199
  refute(@obj.validate {|*a| invoked << a })
200
- assert_equal([["Error: Unexpected value encountered", false, @obj]], invoked)
200
+ assert_equal([["Unexpected error encountered: Unknown", false, @obj]], invoked)
201
201
  end
202
202
  end
203
203
 
@@ -131,9 +131,42 @@ describe HexaPDF::PDFArray do
131
131
  end
132
132
  end
133
133
 
134
- it "allows deleting elements that are selected using a block" do
135
- @array.reject! {|item| item == :data }
136
- assert_equal([1, "deref", @array[2]], @array[0, 5])
134
+ describe "reject!" do
135
+ it "allows deleting elements that are selected using a block" do
136
+ assert_same(@array, @array.reject! {|item| item == :data })
137
+ assert_equal([1, "deref", @array[2]], @array.to_a)
138
+ end
139
+
140
+ it "returns nil if no elements were deleted" do
141
+ assert_nil(@array.reject! {|item| false })
142
+ end
143
+
144
+ it "returns an enumerator if no block is given" do
145
+ assert_kind_of(Enumerator, @array.reject!)
146
+ end
147
+ end
148
+
149
+ describe "map!" do
150
+ it "maps elements in-place to the return values of the block" do
151
+ assert_same(@array, @array.map! {|item| 5 })
152
+ assert_equal([5, 5, 5, 5], @array.to_a)
153
+ end
154
+
155
+ it "returns an enumerator if no block is given" do
156
+ assert_kind_of(Enumerator, @array.reject!)
157
+ end
158
+ end
159
+
160
+ describe "compact!" do
161
+ it "removes all nil elements and returns self" do
162
+ @array << nil
163
+ assert_same(@array, @array.compact!)
164
+ assert_equal(4, @array.size)
165
+ end
166
+
167
+ it "returns nil if no elements were removed" do
168
+ assert_nil(@array.compact!)
169
+ end
137
170
  end
138
171
 
139
172
  describe "index" do
@@ -373,12 +373,87 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
373
373
  describe "push buttons" do
374
374
  before do
375
375
  @field.initialize_as_push_button
376
- @widget = @field.create_widget(@page, Rect: [0, 0, 0, 0])
376
+ @widget = @field.create_widget(@page, Rect: [0, 0, 100, 50])
377
+ @widget.marker_style(style: 'Test')
377
378
  @generator = HexaPDF::Type::AcroForm::AppearanceGenerator.new(@widget)
378
379
  end
379
380
 
380
- it "fails because it is not implemented yet" do
381
- assert_raises(HexaPDF::Error) { @generator.create_appearances }
381
+ it "set the print flag on the widgets" do
382
+ @generator.create_appearances
383
+ assert(@widget.flagged?(:print))
384
+ end
385
+
386
+ it "removes the hidden flag on the widgets" do
387
+ @widget.flag(:hidden)
388
+ @generator.create_appearances
389
+ refute(@widget.flagged?(:hidden))
390
+ end
391
+
392
+ it "adds an appropriate form XObject" do
393
+ @generator.create_appearances
394
+ form = @widget[:AP][:N]
395
+ assert_equal(:XObject, form.type)
396
+ assert_equal(:Form, form[:Subtype])
397
+ assert_equal([0, 0, 100, 50], form[:BBox])
398
+ assert_equal(@doc.acro_form.default_resources[:Font][:F1], form[:Resources][:Font][:F1])
399
+ end
400
+
401
+ it "re-uses the existing form XObject" do
402
+ @generator.create_appearances
403
+ form = @widget[:AP][:N]
404
+ form[:key] = :value
405
+ form.delete(:Subtype)
406
+ @widget[:AP][:N] = @doc.wrap(form, type: HexaPDF::Dictionary)
407
+
408
+ @generator.create_appearances
409
+ assert_equal(form, @widget[:AP][:N])
410
+ refute(form.key?(:key))
411
+ end
412
+
413
+ describe "takes the rotation into account" do
414
+ def check_rotation(angle, width, height, matrix)
415
+ @widget[:MK][:R] = angle
416
+ @generator.create_appearances
417
+ form = @widget[:AP][:N]
418
+ assert_equal([0, 0, width, height], form[:BBox].value)
419
+ assert_equal(matrix, form[:Matrix].value)
420
+ end
421
+
422
+ it "works for 0 degrees" do
423
+ check_rotation(-360, @widget[:Rect].width, @widget[:Rect].height, [1, 0, 0, 1, 0, 0])
424
+ end
425
+
426
+ it "works for 90 degrees" do
427
+ check_rotation(450, @widget[:Rect].height, @widget[:Rect].width, [0, 1, -1, 0, 0, 0])
428
+ end
429
+
430
+ it "works for 180 degrees" do
431
+ check_rotation(180, @widget[:Rect].width, @widget[:Rect].height, [0, -1, -1, 0, 0, 0])
432
+ end
433
+
434
+ it "works for 270 degrees" do
435
+ check_rotation(-90, @widget[:Rect].height, @widget[:Rect].width, [0, -1, 1, 0, 0, 0])
436
+ end
437
+ end
438
+
439
+ it "adds the button title in the center" do
440
+ @generator.create_appearances
441
+ assert_operators(@widget[:AP][:N].stream,
442
+ [[:save_graphics_state],
443
+ [:set_device_gray_non_stroking_color, [0.5]],
444
+ [:append_rectangle, [0, 0, 100, 50]],
445
+ [:fill_path_non_zero],
446
+ [:append_rectangle, [0.5, 0.5, 99.0, 49.0]],
447
+ [:stroke_path],
448
+ [:restore_graphics_state],
449
+ [:save_graphics_state],
450
+ [:set_font_and_size, [:F1, 9]],
451
+ [:begin_text],
452
+ [:move_text, [41.2475, 22.7005]],
453
+ [:show_text, ["Test"]],
454
+ [:end_text],
455
+ [:restore_graphics_state]],
456
+ )
382
457
  end
383
458
  end
384
459
  end
@@ -236,6 +236,13 @@ describe HexaPDF::Type::AcroForm::ButtonField do
236
236
  assert(widget[:AP][:N][:test])
237
237
  end
238
238
 
239
+ it "works for push buttons" do
240
+ @field.initialize_as_push_button
241
+ @field.create_widget(@doc.pages.add, Rect: [0, 0, 100, 50])
242
+ @field.create_appearances
243
+ assert(@field[:AP][:N])
244
+ end
245
+
239
246
  it "won't generate appearances if they already exist" do
240
247
  widget = @field.create_widget(@doc.pages.add, Rect: [0, 0, 0, 0])
241
248
  @field.create_appearances
@@ -262,12 +269,6 @@ describe HexaPDF::Type::AcroForm::ButtonField do
262
269
  refute_same(yes, widget.appearance_dict.normal_appearance[:Yes])
263
270
  end
264
271
 
265
- it "fails for push buttons as they are not implemented yet" do
266
- @field.flag(:push_button)
267
- @field.create_widget(@doc.pages.add, Rect: [0, 0, 0, 0])
268
- assert_raises(HexaPDF::Error) { @field.create_appearances }
269
- end
270
-
271
272
  it "uses the configuration option acro_form.appearance_generator" do
272
273
  @doc.config['acro_form.appearance_generator'] = 'NonExistent'
273
274
  assert_raises(Exception) { @field.create_appearances }
@@ -193,9 +193,14 @@ describe HexaPDF::Type::AcroForm::Field do
193
193
 
194
194
  it "extracts an embedded widget into a standalone object if necessary" do
195
195
  widget1 = @field.create_widget(@page, Rect: [1, 2, 3, 4])
196
+ # Make sure that the field/widget looks like as if it has been loaded from a file
197
+ @doc.revisions.current.update(widget1)
198
+ assert_equal(@field, widget1)
199
+
196
200
  widget2 = @field.create_widget(@doc.pages.add, Rect: [2, 1, 4, 3])
197
201
  kids = @field[:Kids]
198
202
 
203
+ assert_kind_of(HexaPDF::Type::AcroForm::Field, @doc.object(@field.oid))
199
204
  assert_equal(2, kids.length)
200
205
  refute_same(widget1, kids[0])
201
206
  assert_same(widget2, kids[1])
@@ -558,13 +558,23 @@ describe HexaPDF::Type::AcroForm::Form do
558
558
  assert_equal(:Tx4, @acro_form[:Fields][2][:Kids][0][:T])
559
559
  assert_equal(@acro_form[:Fields][2], @acro_form[:Fields][2][:Kids][0][:Parent])
560
560
  end
561
+
562
+ it "ensures that objects loaded as widget are stored as field" do
563
+ @acro_form[:Fields][2] = @doc.add({T: :WidgetField, Type: :Annot, Subtype: :Widget})
564
+ assert_kind_of(HexaPDF::Type::Annotations::Widget, @acro_form[:Fields][2])
565
+
566
+ assert(@acro_form.validate)
567
+ field = @acro_form[:Fields][0]
568
+ assert_kind_of(HexaPDF::Type::AcroForm::Field, field)
569
+ assert_equal(:WidgetField, field.full_field_name)
570
+ end
561
571
  end
562
572
 
563
573
  describe "combining fields with the same name" do
564
574
  before do
565
575
  @acro_form[:Fields] = [
566
576
  @doc.add({T: 'e', Subtype: :Widget, Rect: [0, 0, 0, 1]}),
567
- @doc.add({T: 'e', Subtype: :Widget, Rect: [0, 0, 0, 2]}),
577
+ @merged_field = @doc.add({T: 'e', Subtype: :Widget, Rect: [0, 0, 0, 2]}),
568
578
  @doc.add({T: 'Tx2'}),
569
579
  @doc.add({T: 'e', Kids: [{Subtype: :Widget, Rect: [0, 0, 0, 3]}]}),
570
580
  ]
@@ -576,6 +586,12 @@ describe HexaPDF::Type::AcroForm::Form do
576
586
  assert_equal([[0, 0, 0, 1], [0, 0, 0, 2], [0, 0, 0, 3]],
577
587
  @acro_form.field_by_name('e').each_widget.map {|w| w[:Rect] })
578
588
  end
589
+
590
+ it "deletes the combined and now unneeded field objects" do
591
+ assert(@acro_form.validate)
592
+ assert(@merged_field.null?)
593
+ assert(@doc.object(@merged_field.oid).null?)
594
+ end
579
595
  end
580
596
 
581
597
  describe "automatically creates the terminal fields; appearances" do
@@ -395,4 +395,214 @@ describe HexaPDF::Type::Annotations::AppearanceGenerator do
395
395
  end
396
396
  end
397
397
  end
398
+
399
+ describe "square/circle" do
400
+ before do
401
+ @square = @doc.add({Type: :Annot, Subtype: :Square, Rect: [100, 100, 200, 150], C: [0],
402
+ BS: {W: 2}})
403
+ @generator = HexaPDF::Type::Annotations::AppearanceGenerator.new(@square)
404
+ end
405
+
406
+ it "sets the print flag and unsets the hidden flag" do
407
+ @square.flag(:hidden)
408
+ @generator.create_appearance
409
+ assert(@square.flagged?(:print))
410
+ refute(@square.flagged?(:hidden))
411
+ end
412
+
413
+ it "creates the /RD entry if it doesn't exist and adjusts the /Rect" do
414
+ @generator.create_appearance
415
+ assert_equal([99, 99, 201, 151], @square[:Rect])
416
+ assert_equal([0, 0, 102, 52], @square.appearance[:BBox])
417
+ assert_operators(@square.appearance.stream,
418
+ [[:set_line_width, [2]],
419
+ [:append_rectangle, [1, 1, 100, 50]],
420
+ [:stroke_path]])
421
+ end
422
+
423
+ it "uses an existing /RD entry" do
424
+ @square[:RD] = [2, 4, 6, 8]
425
+ @generator.create_appearance
426
+ assert_equal([100, 100, 200, 150], @square[:Rect])
427
+ assert_equal([0, 0, 100, 50], @square.appearance[:BBox])
428
+ assert_operators(@square.appearance.stream,
429
+ [[:set_line_width, [2]],
430
+ [:append_rectangle, [3, 9, 90, 36]],
431
+ [:stroke_path]])
432
+ end
433
+
434
+ it "can apply just a fill color without a stroke color" do
435
+ @square.delete(:C)
436
+ @square.interior_color(255, 0, 0)
437
+ @generator.create_appearance
438
+ assert_operators(@square.appearance.stream,
439
+ [[:set_device_rgb_non_stroking_color, [1, 0, 0]],
440
+ [:set_line_width, [2]],
441
+ [:append_rectangle, [1, 1, 100, 50]],
442
+ [:fill_path_non_zero]])
443
+ end
444
+
445
+ it "applies all set styling options" do
446
+ @square.border_style(color: [255, 0, 0], width: 10, style: [2, 1])
447
+ @square.interior_color(0, 255, 0)
448
+ @square.opacity(fill_alpha: 0.5, stroke_alpha: 0.5)
449
+ @generator.create_appearance
450
+ assert_operators(@square.appearance.stream,
451
+ [[:set_graphics_state_parameters, [:GS1]],
452
+ [:set_device_rgb_stroking_color, [1, 0, 0]],
453
+ [:set_device_rgb_non_stroking_color, [0, 1, 0]],
454
+ [:set_line_width, [10]],
455
+ [:set_line_dash_pattern, [[2, 1], 0]],
456
+ [:append_rectangle, [5, 5, 100, 50]],
457
+ [:fill_and_stroke_path_non_zero]])
458
+ end
459
+
460
+ it "doesn't draw anything if neither stroke nor fill color is set" do
461
+ @square.delete(:C)
462
+ @generator.create_appearance
463
+ assert_operators(@square.appearance.stream, [])
464
+ end
465
+
466
+ it "draws an ellipse" do
467
+ @square[:Subtype] = :Circle
468
+ @generator.create_appearance
469
+ assert_operators(@square.appearance.stream,
470
+ [[:set_line_width, [2]],
471
+ [:move_to, [101.0, 26.0]],
472
+ [:curve_to, [101.0, 34.920552, 91.45085, 43.190359, 76.0, 47.650635]],
473
+ [:curve_to, [60.54915, 52.110911, 41.45085, 52.110911, 26.0, 47.650635]],
474
+ [:curve_to, [10.54915, 43.190359, 1.0, 34.920552, 1.0, 26.0]],
475
+ [:curve_to, [1.0, 17.079448, 10.54915, 8.809641, 26.0, 4.349365]],
476
+ [:curve_to, [41.45085, -0.110911, 60.54915, -0.110911, 76.0, 4.349365]],
477
+ [:curve_to, [91.45085, 8.809641, 101.0, 17.079448, 101.0, 26.0]],
478
+ [:close_subpath],
479
+ [:stroke_path]])
480
+ end
481
+ end
482
+
483
+ describe "polygon/polyline" do
484
+ before do
485
+ @polyline = @doc.add({Type: :Annot, Subtype: :PolyLine, C: [0],
486
+ Vertices: [100, 100, 200, 150, 210, 80]})
487
+ @generator = HexaPDF::Type::Annotations::AppearanceGenerator.new(@polyline)
488
+ end
489
+
490
+ it "sets the print flag and unsets the hidden flag" do
491
+ @polyline.flag(:hidden)
492
+ @generator.create_appearance
493
+ assert(@polyline.flagged?(:print))
494
+ refute(@polyline.flagged?(:hidden))
495
+ end
496
+
497
+ it "creates a simple polyline" do
498
+ @generator.create_appearance
499
+ assert_equal([96, 76, 214, 154], @polyline[:Rect])
500
+ assert_equal([96, 76, 214, 154], @polyline.appearance[:BBox])
501
+ assert_operators(@polyline.appearance.stream,
502
+ [[:move_to, [100, 100]],
503
+ [:line_to, [200, 150]],
504
+ [:line_to, [210, 80]],
505
+ [:stroke_path]])
506
+ end
507
+
508
+ it "creates a simple polygon" do
509
+ @polyline[:Subtype] = :Polygon
510
+ @generator.create_appearance
511
+ assert_operators(@polyline.appearance.stream,
512
+ [[:move_to, [100, 100]],
513
+ [:line_to, [200, 150]],
514
+ [:line_to, [210, 80]],
515
+ [:close_subpath],
516
+ [:stroke_path]])
517
+ end
518
+
519
+ describe "stroke color" do
520
+ it "uses the specified border color for stroking operations" do
521
+ @polyline.border_style(color: "red")
522
+ @generator.create_appearance
523
+ assert_operators(@polyline.appearance.stream,
524
+ [:set_device_rgb_stroking_color, [1, 0, 0]], range: 0)
525
+ assert_operators(@polyline.appearance.stream,
526
+ [:stroke_path], range: 4)
527
+ end
528
+
529
+ it "works with a transparent border" do
530
+ @polyline.border_style(color: :transparent)
531
+ @generator.create_appearance
532
+ assert_operators(@polyline.appearance.stream, [:end_path], range: 3)
533
+ end
534
+ end
535
+
536
+ describe "interior color" do
537
+ it "uses the specified interior color for non-stroking operations" do
538
+ @polyline[:Subtype] = :Polygon
539
+ @polyline.border_style(color: :transparent)
540
+ @polyline.interior_color("red")
541
+ @generator.create_appearance
542
+ assert_operators(@polyline.appearance.stream,
543
+ [:set_device_rgb_non_stroking_color, [1, 0, 0]], range: 0)
544
+ assert_operators(@polyline.appearance.stream,
545
+ [:fill_path_non_zero], range: 5)
546
+ end
547
+
548
+ it "works together with the stroke color" do
549
+ @polyline[:Subtype] = :Polygon
550
+ @polyline.interior_color("red")
551
+ @generator.create_appearance
552
+ assert_operators(@polyline.appearance.stream,
553
+ [:set_device_rgb_non_stroking_color, [1, 0, 0]], range: 0)
554
+ assert_operators(@polyline.appearance.stream,
555
+ [:fill_and_stroke_path_non_zero], range: 5)
556
+ end
557
+
558
+ it "works if neither interior nor border color is used" do
559
+ @polyline[:Subtype] = :Polygon
560
+ @polyline.interior_color(:transparent)
561
+ @polyline.border_style(color: :transparent)
562
+ @generator.create_appearance
563
+ assert_operators(@polyline.appearance.stream,
564
+ [:end_path], range: 4)
565
+ end
566
+ end
567
+
568
+ it "sets the specified border line width" do
569
+ @polyline.border_style(width: 4)
570
+ @generator.create_appearance
571
+ assert_operators(@polyline.appearance.stream,
572
+ [:set_line_width, [4]], range: 0)
573
+ end
574
+
575
+ it "sets the specified line dash pattern if it is an array" do
576
+ @polyline.border_style(style: [5, 2])
577
+ @generator.create_appearance
578
+ assert_operators(@polyline.appearance.stream,
579
+ [:set_line_dash_pattern, [[5, 2], 0]], range: 0)
580
+ end
581
+
582
+ it "sets the specified opacity" do
583
+ @polyline.opacity(fill_alpha: 0.5, stroke_alpha: 0.5)
584
+ @generator.create_appearance
585
+ assert_operators(@polyline.appearance.stream,
586
+ [:set_graphics_state_parameters, [:GS1]], range: 0)
587
+ end
588
+
589
+ it "draws the specified line ending style" do
590
+ @polyline.line_ending_style(start_style: :open_arrow, end_style: :rclosed_arrow)
591
+ @polyline.border_style(width: 2)
592
+ @polyline.interior_color("red")
593
+ @generator.create_appearance
594
+ assert_equal([86, 52, 238, 158], @polyline[:Rect])
595
+ assert_equal([86, 52, 238, 158], @polyline.appearance[:BBox])
596
+ assert_operators(@polyline.appearance.stream,
597
+ [[:move_to, [109.917818, 115.021215]],
598
+ [:line_to, [100, 100]],
599
+ [:line_to, [117.967662, 98.921525]],
600
+ [:stroke_path],
601
+ [:move_to, [221.114086, 94.158993]],
602
+ [:line_to, [210, 80]],
603
+ [:line_to, [203.294995, 96.704578]],
604
+ [:close_subpath],
605
+ [:fill_and_stroke_path_non_zero]], range: 6..-1)
606
+ end
607
+ end
398
608
  end