hexapdf 0.13.0 → 0.14.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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +51 -0
  3. data/examples/019-acro_form.rb +41 -4
  4. data/lib/hexapdf/cli/split.rb +74 -14
  5. data/lib/hexapdf/document.rb +10 -4
  6. data/lib/hexapdf/layout/text_layouter.rb +2 -2
  7. data/lib/hexapdf/object.rb +22 -0
  8. data/lib/hexapdf/parser.rb +23 -1
  9. data/lib/hexapdf/pdf_array.rb +2 -2
  10. data/lib/hexapdf/type/acro_form/appearance_generator.rb +127 -27
  11. data/lib/hexapdf/type/acro_form/button_field.rb +5 -2
  12. data/lib/hexapdf/type/acro_form/choice_field.rb +64 -10
  13. data/lib/hexapdf/type/acro_form/form.rb +133 -10
  14. data/lib/hexapdf/type/acro_form/text_field.rb +68 -3
  15. data/lib/hexapdf/type/cid_font.rb +1 -1
  16. data/lib/hexapdf/type/font.rb +1 -1
  17. data/lib/hexapdf/type/font_simple.rb +1 -1
  18. data/lib/hexapdf/type/font_type0.rb +3 -3
  19. data/lib/hexapdf/type/form.rb +4 -1
  20. data/lib/hexapdf/type/page.rb +5 -5
  21. data/lib/hexapdf/utils/object_hash.rb +0 -1
  22. data/lib/hexapdf/version.rb +1 -1
  23. data/test/hexapdf/layout/test_text_layouter.rb +9 -1
  24. data/test/hexapdf/test_document.rb +14 -6
  25. data/test/hexapdf/test_object.rb +27 -0
  26. data/test/hexapdf/test_parser.rb +46 -0
  27. data/test/hexapdf/test_pdf_array.rb +1 -1
  28. data/test/hexapdf/test_writer.rb +2 -2
  29. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +286 -34
  30. data/test/hexapdf/type/acro_form/test_button_field.rb +15 -0
  31. data/test/hexapdf/type/acro_form/test_choice_field.rb +92 -9
  32. data/test/hexapdf/type/acro_form/test_form.rb +83 -11
  33. data/test/hexapdf/type/acro_form/test_text_field.rb +75 -1
  34. data/test/hexapdf/type/test_form.rb +7 -0
  35. data/test/hexapdf/utils/test_object_hash.rb +5 -0
  36. data/test/test_helper.rb +2 -0
  37. metadata +4 -3
@@ -233,6 +233,14 @@ describe HexaPDF::Type::AcroForm::ButtonField do
233
233
  refute_same(off, widget.appearance.normal_appearance[:Off])
234
234
  end
235
235
 
236
+ it "always generates appearances if force is true" do
237
+ widget = @field.create_widget(@doc.pages.add, Rect: [0, 0, 0, 0])
238
+ @field.create_appearances
239
+ yes = widget.appearance.normal_appearance[:Yes]
240
+ @field.create_appearances(force: true)
241
+ refute_same(yes, widget.appearance.normal_appearance[:Yes])
242
+ end
243
+
236
244
  it "fails for unsupported button types" do
237
245
  @field.flag(:push_button)
238
246
  @field.create_widget(@doc.pages.add, Rect: [0, 0, 0, 0])
@@ -263,6 +271,13 @@ describe HexaPDF::Type::AcroForm::ButtonField do
263
271
  @field.update_widgets
264
272
  assert_equal(:Yes, widget[:AS])
265
273
  end
274
+
275
+ it "creates the appearances if necessary" do
276
+ widget = @field.create_widget(@doc.pages.add, Rect: [0, 0, 0, 0])
277
+ assert_nil(widget[:AP][:N][:Yes])
278
+ @field.update_widgets
279
+ assert(widget[:AP][:N][:Yes])
280
+ end
266
281
  end
267
282
 
268
283
  describe "validation" do
@@ -36,23 +36,34 @@ describe HexaPDF::Type::AcroForm::ChoiceField do
36
36
 
37
37
  describe "field_value=" do
38
38
  before do
39
- @field.option_items = ["test", "other"]
39
+ @field.option_items = ["test", "something", "other", "neu"]
40
+ end
41
+
42
+ it "updates the widgets to reflect the changed value" do
43
+ @field.initialize_as_combo_box
44
+ widget = @field.create_widget(@doc.pages.add, Rect: [0, 0, 0, 0])
45
+ @field.set_default_appearance_string
46
+ @field.field_value = 'test'
47
+ assert(widget[:AP][:N])
40
48
  end
41
49
 
42
50
  describe "combo_box" do
43
51
  before do
44
52
  @field.initialize_as_combo_box
53
+ @field[:I] = 2
45
54
  end
46
55
 
47
56
  it "can set the value for an uneditable combo box" do
48
57
  @field.field_value = 'test'
49
58
  assert_equal("test", @field[:V])
59
+ assert_nil(@field[:I])
50
60
  end
51
61
 
52
62
  it "can set the value for an editable combo box" do
53
63
  @field.flag(:edit)
54
64
  @field.field_value = 'another'
55
65
  assert_equal("another", @field[:V])
66
+ assert_nil(@field[:I])
56
67
  end
57
68
 
58
69
  it "fails if mulitple values are provided for a combo box" do
@@ -76,8 +87,18 @@ describe HexaPDF::Type::AcroForm::ChoiceField do
76
87
 
77
88
  it "can set a multiple values if the list box is a multi-select" do
78
89
  @field.flag(:multi_select)
79
- @field.field_value = ['test', 'other']
80
- assert_equal(['test', 'other'], @field[:V].value)
90
+ @field.field_value = ['other', 'test']
91
+ assert_equal(['other', 'test'], @field[:V].value)
92
+ assert_equal([0, 2], @field[:I].value)
93
+ end
94
+
95
+ it "can read and set the top index" do
96
+ assert_raises(ArgumentError) { @field.list_box_top_index = 4 }
97
+ @field.delete(:Opt)
98
+ assert_raises(ArgumentError) { @field.list_box_top_index = 0 }
99
+ @field.option_items = [1, 2, 3, 4]
100
+ @field.list_box_top_index = 2
101
+ assert_equal(2, @field.list_box_top_index)
81
102
  end
82
103
 
83
104
  it "fails if mulitple values are provided but the list box is not a multi-select" do
@@ -97,10 +118,29 @@ describe HexaPDF::Type::AcroForm::ChoiceField do
97
118
  assert_raises(HexaPDF::Error) { @field.default_field_value = 'unknown' }
98
119
  end
99
120
 
100
- it "sets and returns the array with the option items" do
101
- assert_equal([], @field.option_items)
102
- @field.option_items = ["H\xe4llo".b, "\xFE\xFF".b << "Töne".encode('UTF-16BE').b]
103
- assert_equal(["Hällo", "Töne"], @field.option_items)
121
+ describe "option items" do
122
+ before do
123
+ @items = [["a", "Zx"], "\xFE\xFF".b << "Töne".encode('UTF-16BE').b, "H\xe4llo".b,]
124
+ end
125
+
126
+ it "sets the option items" do
127
+ @field.option_items = @items
128
+ assert_equal(@items, @field[:Opt].value)
129
+
130
+ @field.flag(:sort)
131
+ @field.option_items = @items
132
+ assert_equal(@items.values_at(2, 1, 0), @field[:Opt].value)
133
+ end
134
+
135
+ it "can retrieve the option items" do
136
+ @field[:Opt] = @items
137
+ assert_equal(["Zx", "Töne", "Hällo"], @field.option_items)
138
+ end
139
+
140
+ it "can retrieve the export values" do
141
+ @field[:Opt] = @items
142
+ assert_equal(["a", "Töne", "Hällo"], @field.export_values)
143
+ end
104
144
  end
105
145
 
106
146
  it "returns the correct concrete field type" do
@@ -121,8 +161,51 @@ describe HexaPDF::Type::AcroForm::ChoiceField do
121
161
  assert(@field[:AP][:N])
122
162
  end
123
163
 
124
- it "fails for list boxes" do
125
- assert_raises(HexaPDF::Error) { @field.create_appearances }
164
+ it "works for list box fields" do
165
+ @field.initialize_as_list_box
166
+ @field.set_default_appearance_string
167
+ @field.create_appearances
168
+ assert(@field[:AP][:N])
169
+ end
170
+
171
+ it "only creates a new appearance if the involved dictionary values have changed per widget" do
172
+ @field.initialize_as_list_box
173
+ @field.set_default_appearance_string
174
+ @field.create_appearances
175
+ appearance_stream = @field[:AP][:N].raw_stream
176
+
177
+ @field.create_appearances
178
+ assert_same(appearance_stream, @field[:AP][:N].raw_stream)
179
+
180
+ do_check = lambda do
181
+ @field.create_appearances
182
+ refute_same(appearance_stream, @field[:AP][:N].raw_stream)
183
+ appearance_stream = @field[:AP][:N].raw_stream
184
+ end
185
+
186
+ @field.option_items = ['a', 'b', 'c']
187
+ do_check.call
188
+
189
+ @field.list_box_top_index = 2
190
+ do_check.call
191
+
192
+ @field.field_value = 'b'
193
+ do_check.call
194
+
195
+ widget = @field.create_widget(@doc.pages.add, Rect: [0, 0, 0, 0])
196
+ assert_nil(widget[:AP])
197
+ @field.create_appearances
198
+ refute_nil(widget[:AP][:N])
199
+ end
200
+
201
+ it "force the creation of appearance streams when force: true" do
202
+ @field.initialize_as_list_box
203
+ @field.set_default_appearance_string
204
+ @field.create_appearances
205
+ appearance_stream = @field[:AP][:N].raw_stream
206
+
207
+ @field.create_appearances(force: true)
208
+ refute_same(appearance_stream, @field[:AP][:N].raw_stream)
126
209
  end
127
210
  end
128
211
 
@@ -115,6 +115,10 @@ describe HexaPDF::Type::AcroForm::Form do
115
115
  end
116
116
 
117
117
  describe "create fields" do
118
+ before do
119
+ @acro_form = @doc.acro_form(create: true)
120
+ end
121
+
118
122
  describe "handles the general case" do
119
123
  it "works for names with a dot" do
120
124
  @acro_form[:Fields] = [{T: "root"}]
@@ -134,9 +138,57 @@ describe HexaPDF::Type::AcroForm::Form do
134
138
  end
135
139
  end
136
140
 
141
+ def applies_variable_text_properties(method, **args)
142
+ field = @acro_form.send(method, "field", **args, font: 'Times')
143
+ font_name, font_size = field.parse_default_appearance_string
144
+ assert_equal(:'Times-Roman', @acro_form.default_resources.font(font_name)[:BaseFont])
145
+ assert_equal(0, font_size)
146
+
147
+ field = @acro_form.send(method, "field", **args, font_size: 10)
148
+ font_name, font_size = field.parse_default_appearance_string
149
+ assert_equal(:Helvetica, @acro_form.default_resources.font(font_name)[:BaseFont])
150
+ assert_equal(10, font_size)
151
+
152
+ field = @acro_form.send(method, "field", **args, font: 'Courier', font_size: 10, align: :center)
153
+ font_name, font_size = field.parse_default_appearance_string
154
+ assert_equal(:Courier, @acro_form.default_resources.font(font_name)[:BaseFont])
155
+ assert_equal(10, font_size)
156
+ assert_equal(:center, field.text_alignment)
157
+ end
158
+
137
159
  it "creates a text field" do
138
160
  field = @acro_form.create_text_field("field")
139
161
  assert_equal(:Tx, field.field_type)
162
+ applies_variable_text_properties(:create_text_field)
163
+ end
164
+
165
+ it "creates a multiline text field" do
166
+ field = @acro_form.create_multiline_text_field("field")
167
+ assert_equal(:Tx, field.field_type)
168
+ assert(field.multiline_text_field?)
169
+ applies_variable_text_properties(:create_multiline_text_field)
170
+ end
171
+
172
+ it "creates a comb text field" do
173
+ field = @acro_form.create_comb_text_field("field", max_chars: 9)
174
+ assert_equal(:Tx, field.field_type)
175
+ assert_equal(9, field[:MaxLen])
176
+ assert(field.comb_text_field?)
177
+ applies_variable_text_properties(:create_comb_text_field, max_chars: 9)
178
+ end
179
+
180
+ it "creates a password field" do
181
+ field = @acro_form.create_password_field("field")
182
+ assert_equal(:Tx, field.field_type)
183
+ assert(field.password_field?)
184
+ applies_variable_text_properties(:create_password_field)
185
+ end
186
+
187
+ it "creates a file select field" do
188
+ field = @acro_form.create_file_select_field("field")
189
+ assert_equal(:Tx, field.field_type)
190
+ assert(field.file_select_field?)
191
+ applies_variable_text_properties(:create_file_select_field)
140
192
  end
141
193
 
142
194
  it "creates a check box" do
@@ -150,13 +202,19 @@ describe HexaPDF::Type::AcroForm::Form do
150
202
  end
151
203
 
152
204
  it "creates a combo box" do
153
- field = @acro_form.create_combo_box("field")
205
+ field = @acro_form.create_combo_box("field", option_items: ['a', 'b', 'c'], editable: true)
154
206
  assert(field.combo_box?)
207
+ assert_equal(['a', 'b', 'c'], field.option_items)
208
+ assert(field.flagged?(:edit))
209
+ applies_variable_text_properties(:create_combo_box)
155
210
  end
156
211
 
157
212
  it "creates a list box" do
158
- field = @acro_form.create_list_box("field")
213
+ field = @acro_form.create_list_box("field", option_items: ['a', 'b', 'c'], multi_select: true)
159
214
  assert(field.list_box?)
215
+ assert_equal(['a', 'b', 'c'], field.option_items)
216
+ assert(field.flagged?(:multi_select))
217
+ applies_variable_text_properties(:create_list_box)
160
218
  end
161
219
  end
162
220
 
@@ -185,15 +243,29 @@ describe HexaPDF::Type::AcroForm::Form do
185
243
  assert(@acro_form[:NeedAppearances])
186
244
  end
187
245
 
188
- it "creates the appearances of all field widgets if necessary" do
189
- tf = @acro_form.create_text_field('test')
190
- tf.set_default_appearance_string
191
- tf.create_widget(@doc.pages.add)
192
- cb = @acro_form.create_check_box('test2')
193
- cb.create_widget(@doc.pages.add)
194
- @acro_form.create_appearances
195
- assert(tf.each_widget.all? {|w| w.appearance.normal_appearance.kind_of?(HexaPDF::Stream) })
196
- assert(cb.each_widget.all? {|w| w.appearance.normal_appearance[:Yes].kind_of?(HexaPDF::Stream) })
246
+ describe "create_appearances" do
247
+ before do
248
+ @tf = @acro_form.create_text_field('test')
249
+ @tf.set_default_appearance_string
250
+ @tf.create_widget(@doc.pages.add)
251
+ @cb = @acro_form.create_check_box('test2')
252
+ @cb.create_widget(@doc.pages.add)
253
+ end
254
+
255
+ it "creates the appearances of all field widgets if necessary" do
256
+ @acro_form.create_appearances
257
+ assert(@tf.each_widget.all? {|w| w.appearance.normal_appearance.kind_of?(HexaPDF::Stream) })
258
+ assert(@cb.each_widget.all? {|w| w.appearance.normal_appearance[:Yes].kind_of?(HexaPDF::Stream) })
259
+ end
260
+
261
+ it "force the creation of appearances if force is true" do
262
+ @acro_form.create_appearances
263
+ text_stream = @tf[:AP][:N].raw_stream
264
+ @acro_form.create_appearances
265
+ assert_same(text_stream, @tf[:AP][:N].raw_stream)
266
+ @acro_form.create_appearances(force: true)
267
+ refute_same(text_stream, @tf[:AP][:N].raw_stream)
268
+ end
197
269
  end
198
270
 
199
271
  describe "perform_validation" do
@@ -21,6 +21,56 @@ describe HexaPDF::Type::AcroForm::TextField do
21
21
  assert_equal(6, @field[:MaxLen])
22
22
  end
23
23
 
24
+ it "can be initialized as a multiline text field" do
25
+ @field.flag(:comb)
26
+ @field.initialize_as_multiline_text_field
27
+ assert(@field.multiline_text_field?)
28
+ end
29
+
30
+ it "can be initialized as comb text field" do
31
+ @field.flag(:multiline)
32
+ @field.initialize_as_comb_text_field
33
+ assert(@field.comb_text_field?)
34
+ end
35
+
36
+ it "can be initialized as password field" do
37
+ @field.flag(:multiline)
38
+ @field[:V] = 'test'
39
+ @field.initialize_as_password_field
40
+ assert_nil(@field[:V])
41
+ assert(@field.password_field?)
42
+ end
43
+
44
+ it "can be initialized as a file select field" do
45
+ @field.flag(:multiline)
46
+ @field.initialize_as_file_select_field
47
+ assert(@field.file_select_field?)
48
+ end
49
+
50
+ it "can check whether the field is a multiline text field" do
51
+ refute(@field.multiline_text_field?)
52
+ @field.flag(:multiline)
53
+ assert(@field.multiline_text_field?)
54
+ end
55
+
56
+ it "can check whether the field is a comb text field" do
57
+ refute(@field.comb_text_field?)
58
+ @field.flag(:comb)
59
+ assert(@field.comb_text_field?)
60
+ end
61
+
62
+ it "can check whether the field is a password field" do
63
+ refute(@field.password_field?)
64
+ @field.flag(:password)
65
+ assert(@field.password_field?)
66
+ end
67
+
68
+ it "can check whether the field is a file select field" do
69
+ refute(@field.file_select_field?)
70
+ @field.flag(:file_select)
71
+ assert(@field.file_select_field?)
72
+ end
73
+
24
74
  describe "field_value" do
25
75
  it "handles unset values" do
26
76
  assert_nil(@field.field_value)
@@ -81,14 +131,38 @@ describe HexaPDF::Type::AcroForm::TextField do
81
131
  end
82
132
 
83
133
  describe "create_appearances" do
84
- it "creates the needed streams" do
134
+ before do
85
135
  @doc.acro_form(create: true)
86
136
  @field.create_widget(@doc.pages.add, Rect: [0, 0, 0, 0])
87
137
  @field.set_default_appearance_string
138
+ end
139
+
140
+ it "creates the needed streams" do
88
141
  @field.create_appearances
89
142
  assert(@field[:AP][:N])
90
143
  end
91
144
 
145
+ it "doesn't create a new appearance stream if the field value hasn't changed, checked per widget" do
146
+ @field.create_appearances
147
+ stream = @field[:AP][:N].raw_stream
148
+ @field.create_appearances
149
+ assert_same(stream, @field[:AP][:N].raw_stream)
150
+ @field.field_value = 'test'
151
+ refute_same(stream, @field[:AP][:N].raw_stream)
152
+
153
+ widget = @field.create_widget(@doc.pages.add, Rect: [0, 0, 0, 0])
154
+ assert_nil(widget[:AP])
155
+ @field.create_appearances
156
+ refute_nil(widget[:AP][:N])
157
+ end
158
+
159
+ it "always creates a new appearance stream if force is true" do
160
+ @field.create_appearances
161
+ stream = @field[:AP][:N].raw_stream
162
+ @field.create_appearances(force: true)
163
+ refute_same(stream, @field[:AP][:N].raw_stream)
164
+ end
165
+
92
166
  it "uses the configuration option acro_form.appearance_generator" do
93
167
  @doc.config['acro_form.appearance_generator'] = 'NonExistent'
94
168
  assert_raises(Exception) { @field.create_appearances }
@@ -43,6 +43,13 @@ describe HexaPDF::Type::Form do
43
43
  @form.contents = 'test'
44
44
  assert_equal('test', @form.stream)
45
45
  end
46
+
47
+ it "clears the cache to make sure that a new canvas can be created" do
48
+ @form[:BBox] = [0, 0, 100, 100]
49
+ canvas = @form.canvas
50
+ @form.contents = ''
51
+ refute_same(canvas, @form.canvas)
52
+ end
46
53
  end
47
54
 
48
55
  describe "resources" do
@@ -112,4 +112,9 @@ describe HexaPDF::Utils::ObjectHash do
112
112
  assert_equal(3, @hash.max_oid)
113
113
  end
114
114
  end
115
+
116
+ it "can return a list of all object IDs" do
117
+ @hash[1, 0] = @hash[3, 1] = 7
118
+ assert_equal([3, 1], @hash.oids)
119
+ end
115
120
  end
@@ -16,6 +16,8 @@ require 'zlib'
16
16
  TEST_DATA_DIR = File.join(__dir__, 'data')
17
17
  MINIMAL_PDF = File.read(File.join(TEST_DATA_DIR, 'minimal.pdf')).freeze
18
18
 
19
+ Minitest::Test.make_my_diffs_pretty!
20
+
19
21
  module TestHelper
20
22
 
21
23
  # Asserts that the method +name+ of +object+ gets invoked with the +expected_values+ when
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hexapdf
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.13.0
4
+ version: 0.14.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Thomas Leitner
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-11-14 00:00:00.000000000 Z
11
+ date: 2020-12-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: cmdparse
@@ -631,6 +631,7 @@ files:
631
631
  homepage: https://hexapdf.gettalong.org
632
632
  licenses:
633
633
  - AGPL-3.0
634
+ - Commercial License
634
635
  metadata: {}
635
636
  post_install_message:
636
637
  rdoc_options: []
@@ -647,7 +648,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
647
648
  - !ruby/object:Gem::Version
648
649
  version: '0'
649
650
  requirements: []
650
- rubygems_version: 3.0.3
651
+ rubygems_version: 3.2.3
651
652
  signing_key:
652
653
  specification_version: 4
653
654
  summary: HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby