hexapdf 0.13.0 → 0.14.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +51 -0
- data/examples/019-acro_form.rb +41 -4
- data/lib/hexapdf/cli/split.rb +74 -14
- data/lib/hexapdf/document.rb +10 -4
- data/lib/hexapdf/layout/text_layouter.rb +2 -2
- data/lib/hexapdf/object.rb +22 -0
- data/lib/hexapdf/parser.rb +23 -1
- data/lib/hexapdf/pdf_array.rb +2 -2
- data/lib/hexapdf/type/acro_form/appearance_generator.rb +127 -27
- data/lib/hexapdf/type/acro_form/button_field.rb +5 -2
- data/lib/hexapdf/type/acro_form/choice_field.rb +64 -10
- data/lib/hexapdf/type/acro_form/form.rb +133 -10
- data/lib/hexapdf/type/acro_form/text_field.rb +68 -3
- data/lib/hexapdf/type/cid_font.rb +1 -1
- data/lib/hexapdf/type/font.rb +1 -1
- data/lib/hexapdf/type/font_simple.rb +1 -1
- data/lib/hexapdf/type/font_type0.rb +3 -3
- data/lib/hexapdf/type/form.rb +4 -1
- data/lib/hexapdf/type/page.rb +5 -5
- data/lib/hexapdf/utils/object_hash.rb +0 -1
- data/lib/hexapdf/version.rb +1 -1
- data/test/hexapdf/layout/test_text_layouter.rb +9 -1
- data/test/hexapdf/test_document.rb +14 -6
- data/test/hexapdf/test_object.rb +27 -0
- data/test/hexapdf/test_parser.rb +46 -0
- data/test/hexapdf/test_pdf_array.rb +1 -1
- data/test/hexapdf/test_writer.rb +2 -2
- data/test/hexapdf/type/acro_form/test_appearance_generator.rb +286 -34
- data/test/hexapdf/type/acro_form/test_button_field.rb +15 -0
- data/test/hexapdf/type/acro_form/test_choice_field.rb +92 -9
- data/test/hexapdf/type/acro_form/test_form.rb +83 -11
- data/test/hexapdf/type/acro_form/test_text_field.rb +75 -1
- data/test/hexapdf/type/test_form.rb +7 -0
- data/test/hexapdf/utils/test_object_hash.rb +5 -0
- data/test/test_helper.rb +2 -0
- 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 = ['
|
80
|
-
assert_equal(['
|
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
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
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 "
|
125
|
-
|
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
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
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
|
-
|
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
|
data/test/test_helper.rb
CHANGED
@@ -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.
|
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
|
+
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.
|
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
|