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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +90 -0
- data/README.md +1 -1
- data/lib/hexapdf/cli/form.rb +9 -4
- data/lib/hexapdf/cli/inspect.rb +13 -4
- data/lib/hexapdf/composer.rb +14 -0
- data/lib/hexapdf/configuration.rb +15 -0
- data/lib/hexapdf/dictionary_fields.rb +1 -1
- data/lib/hexapdf/digital_signature/signing/default_handler.rb +1 -2
- data/lib/hexapdf/document/annotations.rb +107 -2
- data/lib/hexapdf/document/layout.rb +94 -15
- data/lib/hexapdf/document/metadata.rb +10 -3
- data/lib/hexapdf/document.rb +9 -0
- data/lib/hexapdf/encryption/standard_security_handler.rb +7 -2
- data/lib/hexapdf/error.rb +11 -3
- data/lib/hexapdf/font/true_type/subsetter.rb +15 -2
- data/lib/hexapdf/layout/box.rb +5 -0
- data/lib/hexapdf/layout/container_box.rb +63 -28
- data/lib/hexapdf/layout/style.rb +129 -20
- data/lib/hexapdf/layout/table_box.rb +20 -2
- data/lib/hexapdf/object.rb +2 -2
- data/lib/hexapdf/pdf_array.rb +25 -3
- data/lib/hexapdf/tokenizer.rb +4 -1
- data/lib/hexapdf/type/acro_form/appearance_generator.rb +57 -8
- data/lib/hexapdf/type/acro_form/field.rb +1 -0
- data/lib/hexapdf/type/acro_form/form.rb +7 -6
- data/lib/hexapdf/type/annotation.rb +12 -0
- data/lib/hexapdf/type/annotations/appearance_generator.rb +169 -16
- data/lib/hexapdf/type/annotations/border_effect.rb +99 -0
- data/lib/hexapdf/type/annotations/circle.rb +65 -0
- data/lib/hexapdf/type/annotations/interior_color.rb +84 -0
- data/lib/hexapdf/type/annotations/line.rb +5 -192
- data/lib/hexapdf/type/annotations/line_ending_styling.rb +208 -0
- data/lib/hexapdf/type/annotations/markup_annotation.rb +0 -1
- data/lib/hexapdf/type/annotations/polygon.rb +64 -0
- data/lib/hexapdf/type/annotations/polygon_polyline.rb +109 -0
- data/lib/hexapdf/type/annotations/polyline.rb +64 -0
- data/lib/hexapdf/type/annotations/square.rb +65 -0
- data/lib/hexapdf/type/annotations/square_circle.rb +77 -0
- data/lib/hexapdf/type/annotations/widget.rb +50 -20
- data/lib/hexapdf/type/annotations.rb +9 -0
- data/lib/hexapdf/type/measure.rb +57 -0
- data/lib/hexapdf/type.rb +1 -0
- data/lib/hexapdf/version.rb +1 -1
- data/test/hexapdf/digital_signature/signing/test_default_handler.rb +0 -1
- data/test/hexapdf/document/test_annotations.rb +42 -0
- data/test/hexapdf/document/test_layout.rb +38 -10
- data/test/hexapdf/document/test_metadata.rb +13 -1
- data/test/hexapdf/encryption/test_standard_security_handler.rb +2 -1
- data/test/hexapdf/font/true_type/test_subsetter.rb +10 -0
- data/test/hexapdf/layout/test_box.rb +8 -0
- data/test/hexapdf/layout/test_container_box.rb +34 -6
- data/test/hexapdf/layout/test_page_style.rb +1 -1
- data/test/hexapdf/layout/test_style.rb +46 -2
- data/test/hexapdf/layout/test_table_box.rb +14 -1
- data/test/hexapdf/test_composer.rb +7 -0
- data/test/hexapdf/test_dictionary_fields.rb +1 -0
- data/test/hexapdf/test_object.rb +1 -1
- data/test/hexapdf/test_pdf_array.rb +36 -3
- data/test/hexapdf/type/acro_form/test_appearance_generator.rb +78 -3
- data/test/hexapdf/type/acro_form/test_button_field.rb +7 -6
- data/test/hexapdf/type/acro_form/test_field.rb +5 -0
- data/test/hexapdf/type/acro_form/test_form.rb +17 -1
- data/test/hexapdf/type/annotations/test_appearance_generator.rb +210 -0
- data/test/hexapdf/type/annotations/test_border_effect.rb +59 -0
- data/test/hexapdf/type/annotations/test_interior_color.rb +37 -0
- data/test/hexapdf/type/annotations/test_line.rb +0 -45
- data/test/hexapdf/type/annotations/test_line_ending_styling.rb +42 -0
- data/test/hexapdf/type/annotations/test_polygon_polyline.rb +29 -0
- data/test/hexapdf/type/annotations/test_widget.rb +35 -0
- 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([
|
52
|
-
|
53
|
-
|
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(
|
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
|
-
|
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(
|
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],
|
@@ -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(
|
794
|
-
assert_equal([:proportional,
|
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
|
data/test/hexapdf/test_object.rb
CHANGED
@@ -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([["
|
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
|
-
|
135
|
-
|
136
|
-
|
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,
|
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 "
|
381
|
-
|
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
|