hexapdf 0.40.0 → 0.42.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +71 -0
  3. data/examples/019-acro_form.rb +12 -23
  4. data/examples/027-composer_optional_content.rb +1 -1
  5. data/examples/030-pdfa.rb +6 -6
  6. data/examples/031-acro_form_java_script.rb +113 -0
  7. data/lib/hexapdf/cli/command.rb +25 -11
  8. data/lib/hexapdf/cli/files.rb +31 -7
  9. data/lib/hexapdf/cli/form.rb +46 -38
  10. data/lib/hexapdf/cli/info.rb +4 -0
  11. data/lib/hexapdf/cli/inspect.rb +1 -1
  12. data/lib/hexapdf/cli/usage.rb +215 -0
  13. data/lib/hexapdf/cli.rb +2 -0
  14. data/lib/hexapdf/configuration.rb +11 -1
  15. data/lib/hexapdf/content/canvas.rb +2 -0
  16. data/lib/hexapdf/document/layout.rb +8 -1
  17. data/lib/hexapdf/encryption/aes.rb +13 -6
  18. data/lib/hexapdf/encryption/security_handler.rb +6 -4
  19. data/lib/hexapdf/font/cmap/parser.rb +1 -5
  20. data/lib/hexapdf/font/cmap.rb +22 -3
  21. data/lib/hexapdf/font_loader/from_configuration.rb +1 -1
  22. data/lib/hexapdf/font_loader/variant_from_name.rb +72 -0
  23. data/lib/hexapdf/font_loader.rb +1 -0
  24. data/lib/hexapdf/layout/style.rb +5 -4
  25. data/lib/hexapdf/type/acro_form/appearance_generator.rb +11 -76
  26. data/lib/hexapdf/type/acro_form/button_field.rb +7 -5
  27. data/lib/hexapdf/type/acro_form/field.rb +14 -0
  28. data/lib/hexapdf/type/acro_form/form.rb +70 -8
  29. data/lib/hexapdf/type/acro_form/java_script_actions.rb +649 -0
  30. data/lib/hexapdf/type/acro_form/text_field.rb +90 -0
  31. data/lib/hexapdf/type/acro_form.rb +1 -0
  32. data/lib/hexapdf/type/annotations/widget.rb +1 -1
  33. data/lib/hexapdf/type/resources.rb +2 -1
  34. data/lib/hexapdf/utils.rb +19 -0
  35. data/lib/hexapdf/version.rb +1 -1
  36. data/test/hexapdf/encryption/test_aes.rb +18 -8
  37. data/test/hexapdf/encryption/test_security_handler.rb +17 -0
  38. data/test/hexapdf/encryption/test_standard_security_handler.rb +1 -1
  39. data/test/hexapdf/font/cmap/test_parser.rb +5 -3
  40. data/test/hexapdf/font/test_cmap.rb +8 -0
  41. data/test/hexapdf/font_loader/test_from_configuration.rb +4 -0
  42. data/test/hexapdf/font_loader/test_variant_from_name.rb +34 -0
  43. data/test/hexapdf/test_utils.rb +16 -0
  44. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +31 -47
  45. data/test/hexapdf/type/acro_form/test_button_field.rb +5 -0
  46. data/test/hexapdf/type/acro_form/test_field.rb +11 -0
  47. data/test/hexapdf/type/acro_form/test_form.rb +80 -0
  48. data/test/hexapdf/type/acro_form/test_java_script_actions.rb +327 -0
  49. data/test/hexapdf/type/acro_form/test_text_field.rb +62 -0
  50. data/test/hexapdf/type/test_resources.rb +5 -0
  51. metadata +8 -2
@@ -0,0 +1,327 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'test_helper'
4
+ require 'hexapdf/document'
5
+ require 'hexapdf/type/acro_form/java_script_actions'
6
+
7
+ describe HexaPDF::Type::AcroForm::JavaScriptActions do
8
+ before do
9
+ @klass = HexaPDF::Type::AcroForm::JavaScriptActions
10
+ @action = {S: :JavaScript}
11
+ end
12
+
13
+ describe "format" do
14
+ it "returns the original value if the format action can't be processed" do
15
+ @action[:JS] = 'Unknown();'
16
+ @klass.apply_format("10", @action)
17
+ end
18
+
19
+ describe "AFNumber_Format" do
20
+ before do
21
+ @value = '1234567.898765'
22
+ @action[:JS] = ''
23
+ end
24
+
25
+ it "returns a correct JavaScript string" do
26
+ assert_equal('AFNumber_Format(2, 0, 0, 0, "", true);',
27
+ @klass.af_number_format_action)
28
+ assert_equal('AFNumber_Format(1, 1, 1, 0, "E", false);',
29
+ @klass.af_number_format_action(decimals: 1, separator_style: :point_no_thousands,
30
+ negative_style: :red, currency_string: "E",
31
+ prepend_currency: false))
32
+ end
33
+
34
+ it "raise an error for invalid arguments" do
35
+ assert_raises(ArgumentError) { @klass.af_number_format_action(separator_style: :unknown) }
36
+ assert_raises(ArgumentError) { @klass.af_number_format_action(negative_style: :unknown) }
37
+ end
38
+
39
+ def assert_format(arg_string, result_value, result_color)
40
+ @action[:JS] = "AFNumber_Format(#{arg_string});"
41
+ value, text_color = @klass.apply_format(@value, @action)
42
+ assert_equal(result_value, value)
43
+ result_color ? assert_equal(result_color, text_color) : assert_nil(text_color)
44
+ end
45
+
46
+ it "works with both commas and points as decimal separator" do
47
+ @value = '1234567.898'
48
+ assert_format('2, 2, 0, 0, "", false', "1.234.567,90", "black")
49
+ @value = '1234567,898'
50
+ assert_format('2, 2, 0, 0, "", false', "1.234.567,90", "black")
51
+ @value = '123,4567,898'
52
+ assert_format('2, 2, 0, 0, "", false', "123,46", "black")
53
+ end
54
+
55
+ it "respects the set number of decimals" do
56
+ assert_format('0, 2, 0, 0, "E", false', "1.234.568E", "black")
57
+ assert_format('2, 2, 0, 0, "E", false', "1.234.567,90E", "black")
58
+ end
59
+
60
+ it "respects the digit separator style" do
61
+ ["1,234,567.90", "1234567.90", "1.234.567,90", "1234567,90"].each_with_index do |result, style|
62
+ assert_format("2, #{style}, 0, 0, \"\", false", result, "black")
63
+ end
64
+ end
65
+
66
+ it "respects the negative value styling" do
67
+ @value = '-1234567.898'
68
+ [["-E1234567,90", "black"], ["E1234567,90", "red"], ["(E1234567,90)", "black"],
69
+ ["(E1234567,90)", "red"]].each_with_index do |result, style|
70
+ assert_format("2, 3, #{style}, 0, \"E\", true", result[0], result[1])
71
+ end
72
+ end
73
+
74
+ it "respects the specified currency string and position" do
75
+ assert_format('2, 3, 0, 0, " E", false', "1234567,90 E", "black")
76
+ assert_format('2, 3, 0, 0, "E ", true', "E 1234567,90", "black")
77
+ end
78
+
79
+ it "allows omitting the trailing semicolon" do
80
+ @action[:JS] = "AFNumber_Format(2,2,0,0,\"\",false )"
81
+ value, = @klass.apply_format('1234.567', @action)
82
+ assert_equal('1.234,57', value)
83
+ end
84
+
85
+ it "does nothing to the value if the JavaScript method could not be determined " do
86
+ assert_format('2, 3, 0, 0, " E", false, a', "1234567.898765", nil)
87
+ end
88
+ end
89
+
90
+ describe "AFPercent_Format" do
91
+ before do
92
+ @value = '123.456789'
93
+ @action[:JS] = ''
94
+ end
95
+
96
+ it "returns a correct JavaScript string" do
97
+ assert_equal('AFPercent_Format(2, 0);',
98
+ @klass.af_percent_format_action)
99
+ assert_equal('AFPercent_Format(1, 1);',
100
+ @klass.af_percent_format_action(decimals: 1, separator_style: :point_no_thousands))
101
+ end
102
+
103
+ it "raise an error for invalid arguments" do
104
+ assert_raises(ArgumentError) { @klass.af_percent_format_action(separator_style: :unknown) }
105
+ end
106
+
107
+ def assert_format(arg_string, result_value)
108
+ @action[:JS] = "AFPercent_Format(#{arg_string});"
109
+ value, text_color = @klass.apply_format(@value, @action)
110
+ assert_equal(result_value, value)
111
+ assert_nil(text_color)
112
+ end
113
+
114
+ it "works with both commas and points as decimal separator" do
115
+ @value = '123.456789'
116
+ assert_format('2, 2', "12.345,68%")
117
+ @value = '123,456789'
118
+ assert_format('2, 2', "12.345,68%")
119
+ @value = '123,4567,89'
120
+ assert_format('2, 2', "12.345,67%")
121
+ end
122
+
123
+ it "respects the set number of decimals" do
124
+ assert_format('0, 2', "12.346%")
125
+ assert_format('2, 2', "12.345,68%")
126
+ end
127
+
128
+ it "respects the digit separator style" do
129
+ ["12,345.68%", "12345.68%", "12.345,68%", "12345,68%"].each_with_index do |result, style|
130
+ assert_format("2, #{style}", result)
131
+ end
132
+ end
133
+
134
+ it "allows omitting the trailing semicolon" do
135
+ @action[:JS] = "AFPercent_Format(2,2 )"
136
+ value, = @klass.apply_format('1.234', @action)
137
+ assert_equal('123,40%', value)
138
+ end
139
+
140
+ it "does nothing to the value if the JavaScript method could not be determined " do
141
+ assert_format('2, "df"', "123.456789")
142
+ end
143
+ end
144
+
145
+ describe "AFTime_Format" do
146
+ before do
147
+ @value = '15:25:37'
148
+ @action[:JS] = ''
149
+ end
150
+
151
+ it "returns a correct JavaScript string" do
152
+ assert_equal('AFTime_Format(0);',
153
+ @klass.af_time_format_action)
154
+ assert_equal('AFTime_Format(1);',
155
+ @klass.af_time_format_action(format: :hh12_mm))
156
+ end
157
+
158
+ it "raise an error for invalid arguments" do
159
+ assert_raises(ArgumentError) { @klass.af_time_format_action(format: :unknown) }
160
+ end
161
+
162
+ def assert_format(arg_string, result_value)
163
+ @action[:JS] = "AFTime_Format(#{arg_string});"
164
+ value, text_color = @klass.apply_format(@value, @action)
165
+ assert_equal(result_value, value)
166
+ assert_nil(text_color)
167
+ end
168
+
169
+ it "respects the time format" do
170
+ ["15:25", "3:25 PM", "15:25:37", "3:25:37 PM"].each_with_index do |result, style|
171
+ assert_format(style, result)
172
+ end
173
+ end
174
+
175
+ it "allows omitting the trailing semicolon" do
176
+ @action[:JS] = "AFTime_Format(2 )"
177
+ value, = @klass.apply_format('15:34', @action)
178
+ assert_equal('15:34:00', value)
179
+ end
180
+
181
+ it "does nothing to the value if the JavaScript method could not be determined " do
182
+ assert_format('1, "df"', "15:25:37")
183
+ end
184
+ end
185
+ end
186
+
187
+ describe "calculate" do
188
+ before do
189
+ @doc = HexaPDF::Document.new
190
+ @form = @doc.acro_form(create: true)
191
+ @form.create_text_field('text')
192
+ @field1 = @form.create_text_field('text.1')
193
+ @field1.field_value = "10"
194
+ @field2 = @form.create_text_field('text.2')
195
+ @field2.field_value = "20"
196
+ @field3 = @form.create_text_field('text.3')
197
+ @field3.field_value = "30"
198
+ end
199
+
200
+ it "returns nil if the calculate action is not a JavaScript action" do
201
+ @action[:S] = :GoTo
202
+ assert_nil(@klass.calculate(@form, @action))
203
+ end
204
+
205
+ it "returns nil if the calculate action contains unknown JavaScript" do
206
+ @action[:JS] = 'Unknown();'
207
+ assert_nil(@klass.calculate(@form, @action))
208
+ end
209
+
210
+ describe "predefined calculations" do
211
+ it "returns a correct JavaScript string" do
212
+ assert_equal('AFSimple_Calculate("SUM", ["text.1","text.2"]);',
213
+ @klass.af_simple_calculate_action(:sum, ['text.1', @field2]))
214
+ end
215
+
216
+ def assert_calculation(function, fields, value)
217
+ fields = fields.map {|field| "\"#{field.full_field_name}\"" }.join(", ")
218
+ @action[:JS] = "AFSimple_Calculate(\"#{function}\", new Array(#{fields}));"
219
+ assert_equal(value, @klass.calculate(@form, @action))
220
+ end
221
+
222
+ it "can sum fields" do
223
+ assert_calculation('SUM', [@field1, @field2, @field3], "60")
224
+ end
225
+
226
+ it "can average fields" do
227
+ assert_calculation('AVG', [@field1, @field2, @field3], "20")
228
+ end
229
+
230
+ it "can multiply fields" do
231
+ assert_calculation('PRD', [@field1, @field2, @field3], "6000")
232
+ end
233
+
234
+ it "can find the minimum field value" do
235
+ assert_calculation('MIN', [@field1, @field2, @field3], "10")
236
+ end
237
+
238
+ it "can find the maximum field value" do
239
+ assert_calculation('MAX', [@field1, @field2, @field3], "30")
240
+ end
241
+
242
+ it "works with floats" do
243
+ @field1.field_value = "10,54"
244
+ assert_calculation('SUM', [@field1, @field2], "30.54")
245
+ end
246
+
247
+ it "returns nil if a field cannot be resolved" do
248
+ @action[:JS] = 'AFSimple_Calculate("SUM", ["unknown"]);'
249
+ assert_nil(@klass.calculate(@form, @action))
250
+ end
251
+
252
+ it "allows omitting the trailing semicolon" do
253
+ @action[:JS] = 'AFSimple_Calculate("SUM", ["text.1"] )'
254
+ assert_equal("10", @klass.calculate(@form, @action))
255
+ end
256
+ end
257
+
258
+ describe "simplified field notation calculations" do
259
+ it "returns a correct JavaScript string" do
260
+ sfn = '(text.1 + text.2) * text.3 - text.1 / text.1 + 0 + 5.43 + 7,24'
261
+ assert_equal("/** BVCALC #{sfn} EVCALC **/ " \
262
+ 'event.value = (AFMakeNumber(getField("text.1").value) + ' \
263
+ 'AFMakeNumber(getField("text.2").value)) * ' \
264
+ 'AFMakeNumber(getField("text.3").value) - ' \
265
+ 'AFMakeNumber(getField("text.1").value) / ' \
266
+ 'AFMakeNumber(getField("text.1").value) ' \
267
+ '+ 0.0 + 5.43 + 7.24',
268
+ @klass.simplified_field_notation_action(@form, sfn))
269
+ end
270
+
271
+ it "fails if the SFN string is invalid when generating a JavaScript action string" do
272
+ assert_raises(ArgumentError) { @klass.simplified_field_notation_action(@form, '(test') }
273
+ end
274
+
275
+ def assert_calculation(sfn, value)
276
+ @action[:JS] = "/** BVCALC #{sfn} EVCALC **/"
277
+ result = @klass.calculate(@form, @action)
278
+ value ? assert_equal(value, result) : assert_nil(result)
279
+ end
280
+
281
+ it "works for additions" do
282
+ assert_calculation('text.1 + text.2 + text.1', "40")
283
+ end
284
+
285
+ it "works for substraction" do
286
+ assert_calculation('text.2-text\.1', "10")
287
+ end
288
+
289
+ it "works for multiplication" do
290
+ assert_calculation('text.2\* text\.1 * text\.3', "6000")
291
+ end
292
+
293
+ it "works for division" do
294
+ assert_calculation('text.2 /text\.1', "2")
295
+ end
296
+
297
+ it "works with parentheses" do
298
+ assert_calculation('(text.2 + (text.1*text.3))', "320")
299
+ end
300
+
301
+ it "works with numbers" do
302
+ assert_calculation('text.1 + 10.54 - 1,54 + 3', "22")
303
+ end
304
+
305
+ it "works in a more complex case" do
306
+ assert_calculation('(text.1 + text.2)/(text.3) * text.1', "10")
307
+ end
308
+
309
+ it "works with floats" do
310
+ @field1.field_value = "10,54"
311
+ assert_calculation('text.1 + text.2', "30.54")
312
+ end
313
+
314
+ it "fails if a referenced field is not a terminal field" do
315
+ assert_calculation('text + text.2', nil)
316
+ end
317
+
318
+ it "fails if a referenced field does not exist" do
319
+ assert_calculation('unknown + text.2', nil)
320
+ end
321
+
322
+ it "fails if parentheses don't match" do
323
+ assert_calculation('(text.1 + text.2', nil)
324
+ end
325
+ end
326
+ end
327
+ end
@@ -114,6 +114,12 @@ describe HexaPDF::Type::AcroForm::TextField do
114
114
  assert(widget[:AP][:N])
115
115
  end
116
116
 
117
+ it "calls acro_form.on_invalid_value if the provided value is not a string" do
118
+ @doc.config['acro_form.on_invalid_value'] = proc {|_field, value| value.to_s }
119
+ @field.field_value = 10
120
+ assert_equal("10", @field.field_value)
121
+ end
122
+
117
123
  it "fails if the :password flag is set" do
118
124
  @field.flag(:password)
119
125
  assert_raises(HexaPDF::Error) { @field.field_value = 'test' }
@@ -197,6 +203,62 @@ describe HexaPDF::Type::AcroForm::TextField do
197
203
  end
198
204
  end
199
205
 
206
+ describe "set_format_action" do
207
+ it "applies the number format" do
208
+ @doc.acro_form(create: true)
209
+ @field.set_format_action(:number, decimals: 0)
210
+ assert(@field.key?(:AA))
211
+ assert(@field[:AA].key?(:F))
212
+ assert_equal('AFNumber_Format(0, 0, 0, 0, "", true);', @field[:AA][:F][:JS])
213
+ end
214
+
215
+ it "applies the percent format" do
216
+ @doc.acro_form(create: true)
217
+ @field.set_format_action(:percent, decimals: 0)
218
+ assert(@field.key?(:AA))
219
+ assert(@field[:AA].key?(:F))
220
+ assert_equal('AFPercent_Format(0, 0);', @field[:AA][:F][:JS])
221
+ end
222
+
223
+ it "applies the time format" do
224
+ @doc.acro_form(create: true)
225
+ @field.set_format_action(:time, format: :hh_mm_ss)
226
+ assert(@field.key?(:AA))
227
+ assert(@field[:AA].key?(:F))
228
+ assert_equal('AFTime_Format(2);', @field[:AA][:F][:JS])
229
+ end
230
+
231
+ it "fails if an unknown format action is specified" do
232
+ assert_raises(ArgumentError) { @field.set_format_action(:unknown) }
233
+ end
234
+ end
235
+
236
+ describe "set_calculate_action" do
237
+ before do
238
+ @form = @doc.acro_form(create: true)
239
+ @form.create_text_field('text1')
240
+ @form.create_text_field('text2')
241
+ end
242
+
243
+ it "sets the calculate action using AFSimple_Calculate" do
244
+ @field.set_calculate_action(:sum, fields: ['text1', @form.field_by_name('text2')])
245
+ assert(@field.key?(:AA))
246
+ assert(@field[:AA].key?(:C))
247
+ assert_equal('AFSimple_Calculate("SUM", ["text1","text2"]);', @field[:AA][:C][:JS])
248
+ assert_equal([@field], @form[:CO].value)
249
+ end
250
+
251
+ it "sets the simplified field notation calculate action" do
252
+ @field.set_calculate_action(:sfn, fields: "text1")
253
+ assert_equal('/** BVCALC text1 EVCALC **/ event.value = AFMakeNumber(getField("text1").value)',
254
+ @field[:AA][:C][:JS])
255
+ end
256
+
257
+ it "fails if an unknown calculate action is specified" do
258
+ assert_raises(ArgumentError) { @field.set_calculate_action(:unknown) }
259
+ end
260
+ end
261
+
200
262
  describe "validation" do
201
263
  it "checks the value of the /FT field" do
202
264
  @field.delete(:FT)
@@ -146,6 +146,11 @@ describe HexaPDF::Type::Resources do
146
146
  @res.font(:test)
147
147
  end
148
148
  end
149
+
150
+ it "wraps the resulting object in the appropriate font class" do
151
+ @res[:Font] = {test: {Type: :Font, Subtype: :Type1}}
152
+ assert_kind_of(HexaPDF::Type::FontType1, @res.font(:test))
153
+ end
149
154
  end
150
155
 
151
156
  describe "add_font" do
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.40.0
4
+ version: 0.42.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: 2024-03-23 00:00:00.000000000 Z
11
+ date: 2024-05-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: cmdparse
@@ -306,6 +306,7 @@ files:
306
306
  - examples/028-frame_mask_mode.rb
307
307
  - examples/029-composer_fallback_fonts.rb
308
308
  - examples/030-pdfa.rb
309
+ - examples/031-acro_form_java_script.rb
309
310
  - examples/emoji-smile.png
310
311
  - examples/emoji-wink.png
311
312
  - examples/machupicchu.jpg
@@ -324,6 +325,7 @@ files:
324
325
  - lib/hexapdf/cli/modify.rb
325
326
  - lib/hexapdf/cli/optimize.rb
326
327
  - lib/hexapdf/cli/split.rb
328
+ - lib/hexapdf/cli/usage.rb
327
329
  - lib/hexapdf/cli/watermark.rb
328
330
  - lib/hexapdf/composer.rb
329
331
  - lib/hexapdf/configuration.rb
@@ -429,6 +431,7 @@ files:
429
431
  - lib/hexapdf/font_loader/from_configuration.rb
430
432
  - lib/hexapdf/font_loader/from_file.rb
431
433
  - lib/hexapdf/font_loader/standard14.rb
434
+ - lib/hexapdf/font_loader/variant_from_name.rb
432
435
  - lib/hexapdf/image_loader.rb
433
436
  - lib/hexapdf/image_loader/jpeg.rb
434
437
  - lib/hexapdf/image_loader/pdf.rb
@@ -477,6 +480,7 @@ files:
477
480
  - lib/hexapdf/type/acro_form/choice_field.rb
478
481
  - lib/hexapdf/type/acro_form/field.rb
479
482
  - lib/hexapdf/type/acro_form/form.rb
483
+ - lib/hexapdf/type/acro_form/java_script_actions.rb
480
484
  - lib/hexapdf/type/acro_form/signature_field.rb
481
485
  - lib/hexapdf/type/acro_form/text_field.rb
482
486
  - lib/hexapdf/type/acro_form/variable_text_field.rb
@@ -698,6 +702,7 @@ files:
698
702
  - test/hexapdf/font_loader/test_from_configuration.rb
699
703
  - test/hexapdf/font_loader/test_from_file.rb
700
704
  - test/hexapdf/font_loader/test_standard14.rb
705
+ - test/hexapdf/font_loader/test_variant_from_name.rb
701
706
  - test/hexapdf/image_loader/test_jpeg.rb
702
707
  - test/hexapdf/image_loader/test_pdf.rb
703
708
  - test/hexapdf/image_loader/test_png.rb
@@ -748,6 +753,7 @@ files:
748
753
  - test/hexapdf/type/acro_form/test_choice_field.rb
749
754
  - test/hexapdf/type/acro_form/test_field.rb
750
755
  - test/hexapdf/type/acro_form/test_form.rb
756
+ - test/hexapdf/type/acro_form/test_java_script_actions.rb
751
757
  - test/hexapdf/type/acro_form/test_signature_field.rb
752
758
  - test/hexapdf/type/acro_form/test_text_field.rb
753
759
  - test/hexapdf/type/acro_form/test_variable_text_field.rb