hexapdf 0.26.2 → 0.28.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (83) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +115 -1
  3. data/README.md +1 -1
  4. data/examples/013-text_layouter_shapes.rb +8 -8
  5. data/examples/016-frame_automatic_box_placement.rb +3 -3
  6. data/examples/017-frame_text_flow.rb +3 -3
  7. data/examples/019-acro_form.rb +14 -3
  8. data/examples/020-column_box.rb +3 -3
  9. data/examples/023-images.rb +30 -0
  10. data/lib/hexapdf/cli/info.rb +5 -1
  11. data/lib/hexapdf/cli/inspect.rb +2 -2
  12. data/lib/hexapdf/cli/split.rb +8 -8
  13. data/lib/hexapdf/cli/watermark.rb +2 -2
  14. data/lib/hexapdf/configuration.rb +3 -2
  15. data/lib/hexapdf/content/canvas.rb +8 -3
  16. data/lib/hexapdf/dictionary.rb +4 -17
  17. data/lib/hexapdf/document/destinations.rb +42 -5
  18. data/lib/hexapdf/document/signatures.rb +265 -48
  19. data/lib/hexapdf/document.rb +6 -10
  20. data/lib/hexapdf/filter/ascii85_decode.rb +1 -1
  21. data/lib/hexapdf/importer.rb +35 -27
  22. data/lib/hexapdf/layout/list_box.rb +1 -5
  23. data/lib/hexapdf/object.rb +5 -0
  24. data/lib/hexapdf/parser.rb +14 -0
  25. data/lib/hexapdf/revision.rb +15 -12
  26. data/lib/hexapdf/revisions.rb +7 -1
  27. data/lib/hexapdf/tokenizer.rb +15 -9
  28. data/lib/hexapdf/type/acro_form/appearance_generator.rb +174 -128
  29. data/lib/hexapdf/type/acro_form/button_field.rb +5 -3
  30. data/lib/hexapdf/type/acro_form/choice_field.rb +2 -0
  31. data/lib/hexapdf/type/acro_form/field.rb +11 -5
  32. data/lib/hexapdf/type/acro_form/form.rb +61 -8
  33. data/lib/hexapdf/type/acro_form/signature_field.rb +2 -0
  34. data/lib/hexapdf/type/acro_form/text_field.rb +12 -2
  35. data/lib/hexapdf/type/annotations/widget.rb +3 -0
  36. data/lib/hexapdf/type/catalog.rb +1 -1
  37. data/lib/hexapdf/type/font_true_type.rb +14 -0
  38. data/lib/hexapdf/type/object_stream.rb +2 -2
  39. data/lib/hexapdf/type/outline.rb +19 -1
  40. data/lib/hexapdf/type/outline_item.rb +72 -14
  41. data/lib/hexapdf/type/page.rb +95 -64
  42. data/lib/hexapdf/type/resources.rb +13 -17
  43. data/lib/hexapdf/type/signature/adbe_pkcs7_detached.rb +16 -2
  44. data/lib/hexapdf/type/signature.rb +10 -0
  45. data/lib/hexapdf/version.rb +1 -1
  46. data/lib/hexapdf/writer.rb +5 -3
  47. data/test/hexapdf/content/test_canvas.rb +5 -0
  48. data/test/hexapdf/document/test_destinations.rb +41 -0
  49. data/test/hexapdf/document/test_pages.rb +2 -2
  50. data/test/hexapdf/document/test_signatures.rb +139 -19
  51. data/test/hexapdf/encryption/test_aes.rb +1 -1
  52. data/test/hexapdf/filter/test_predictor.rb +0 -1
  53. data/test/hexapdf/layout/test_box.rb +2 -1
  54. data/test/hexapdf/layout/test_column_box.rb +1 -1
  55. data/test/hexapdf/layout/test_list_box.rb +1 -1
  56. data/test/hexapdf/test_document.rb +2 -8
  57. data/test/hexapdf/test_importer.rb +27 -6
  58. data/test/hexapdf/test_parser.rb +19 -2
  59. data/test/hexapdf/test_revision.rb +15 -14
  60. data/test/hexapdf/test_revisions.rb +63 -12
  61. data/test/hexapdf/test_stream.rb +1 -1
  62. data/test/hexapdf/test_tokenizer.rb +10 -1
  63. data/test/hexapdf/test_writer.rb +11 -3
  64. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +135 -56
  65. data/test/hexapdf/type/acro_form/test_button_field.rb +6 -1
  66. data/test/hexapdf/type/acro_form/test_choice_field.rb +4 -0
  67. data/test/hexapdf/type/acro_form/test_field.rb +4 -4
  68. data/test/hexapdf/type/acro_form/test_form.rb +65 -0
  69. data/test/hexapdf/type/acro_form/test_signature_field.rb +4 -0
  70. data/test/hexapdf/type/acro_form/test_text_field.rb +13 -0
  71. data/test/hexapdf/type/signature/common.rb +54 -0
  72. data/test/hexapdf/type/signature/test_adbe_pkcs7_detached.rb +21 -0
  73. data/test/hexapdf/type/test_catalog.rb +5 -2
  74. data/test/hexapdf/type/test_font_true_type.rb +20 -0
  75. data/test/hexapdf/type/test_object_stream.rb +2 -1
  76. data/test/hexapdf/type/test_outline.rb +4 -1
  77. data/test/hexapdf/type/test_outline_item.rb +62 -1
  78. data/test/hexapdf/type/test_page.rb +103 -45
  79. data/test/hexapdf/type/test_page_tree_node.rb +4 -2
  80. data/test/hexapdf/type/test_resources.rb +0 -5
  81. data/test/hexapdf/type/test_signature.rb +8 -0
  82. data/test/test_helper.rb +1 -1
  83. metadata +61 -4
@@ -4,7 +4,6 @@ require_relative 'common'
4
4
  require 'hexapdf/filter/predictor'
5
5
 
6
6
  describe HexaPDF::Filter::Predictor do
7
-
8
7
  module CommonPredictorTests
9
8
  def test_decoding_through_decoder_method
10
9
  @testcases.each do |name, data|
@@ -131,7 +131,8 @@ describe HexaPDF::Layout::Box do
131
131
  assert_equal([nil, box], box.split(150, 150, nil))
132
132
  end
133
133
 
134
- it "can't be split if it doesn't (completely) fit as the default implementation knows nothing about the content" do
134
+ it "can't be split if it doesn't (completely) fit as the default implementation " \
135
+ "knows nothing about the content" do
135
136
  @box.style.position = :flow # make sure we would generally be splitable
136
137
  @box.fit(90, 100, nil)
137
138
  assert_equal([nil, @box], @box.split(150, 150, nil))
@@ -11,7 +11,7 @@ describe HexaPDF::Layout::ColumnBox do
11
11
  @text_boxes = 5.times.map do
12
12
  HexaPDF::Layout::TextBox.new(items: [inline_box] * 15, style: {position: :default})
13
13
  end
14
- draw_block = lambda do |canvas, box|
14
+ draw_block = lambda do |canvas, _box|
15
15
  canvas.move_to(0, 0).end_path
16
16
  end
17
17
  @fixed_size_boxes = 15.times.map { HexaPDF::Layout::Box.new(width: 20, height: 10, &draw_block) }
@@ -222,7 +222,7 @@ describe HexaPDF::Layout::ListBox do
222
222
  end
223
223
 
224
224
  it "allows drawing custom markers" do
225
- marker = lambda do |doc, list_box, index|
225
+ marker = lambda do |_doc, _list_box, _index|
226
226
  HexaPDF::Layout::Box.create(width: 10, height: 10) {}
227
227
  end
228
228
  box = create_box(children: @fixed_size_boxes[0, 1], item_type: marker)
@@ -163,14 +163,8 @@ describe HexaPDF::Document do
163
163
  refute_equal(0, obj.oid)
164
164
  end
165
165
 
166
- it "fails if the given object is not a PDF object" do
167
- assert_raises(ArgumentError) { @doc.import(5) }
168
- end
169
-
170
- it "fails if the given object is associated with no or the destination document" do
171
- assert_raises(ArgumentError) { @doc.import(HexaPDF::Object.new(5)) }
172
- obj = @doc.add(5)
173
- assert_raises(ArgumentError) { @doc.import(obj) }
166
+ it "works if the given object is not a PDF object" do
167
+ assert_equal(5, @doc.import(5))
174
168
  end
175
169
  end
176
170
 
@@ -19,6 +19,7 @@ describe HexaPDF::Importer::NullableWeakRef do
19
19
  end
20
20
 
21
21
  describe HexaPDF::Importer do
22
+ class TestClass < HexaPDF::Dictionary; end
22
23
  before do
23
24
  @source = HexaPDF::Document.new
24
25
  obj = @source.add("test")
@@ -30,12 +31,12 @@ describe HexaPDF::Importer do
30
31
  @source.pages.add
31
32
  @source.pages.root[:Rotate] = 90
32
33
  @dest = HexaPDF::Document.new
33
- @importer = HexaPDF::Importer.for(source: @source, destination: @dest)
34
+ @importer = HexaPDF::Importer.for(@dest)
34
35
  end
35
36
 
36
37
  describe "::for" do
37
38
  it "caches the importer" do
38
- assert_same(@importer, HexaPDF::Importer.for(source: @source, destination: @dest))
39
+ assert_same(@importer, HexaPDF::Importer.for(@dest))
39
40
  end
40
41
  end
41
42
 
@@ -60,8 +61,14 @@ describe HexaPDF::Importer do
60
61
  end
61
62
 
62
63
  it "can import a direct object" do
63
- obj = @importer.import(key: @obj)
64
- assert(@dest.object?(obj[:key]))
64
+ assert_nil(@importer.import(nil))
65
+ assert_equal(5, @importer.import(5))
66
+ assert(@dest.object?(@importer.import({key: @obj})[:key]))
67
+ end
68
+
69
+ it "determines the source document dynamically" do
70
+ obj = @importer.import(@obj.value)
71
+ assert_equal("test", obj[:ref].value)
65
72
  end
66
73
 
67
74
  it "copies the data of the imported objects" do
@@ -86,6 +93,19 @@ describe HexaPDF::Importer do
86
93
  assert_same(hash, obj[:hash])
87
94
  end
88
95
 
96
+ it "uses the class of the argument when directly importing a HexaPDF::Object" do
97
+ src_obj = @source.wrap(@hash, type: TestClass)
98
+ dest_obj = @importer.import(src_obj)
99
+ assert_instance_of(TestClass, dest_obj)
100
+ end
101
+
102
+ it "uses the class of the argument when importing an already mapped HexaPDF::Object" do
103
+ @importer.import(@obj) # also maps @hash
104
+ src_obj = @source.wrap(@hash, type: TestClass)
105
+ dest_obj = @importer.import(src_obj)
106
+ assert_instance_of(TestClass, dest_obj)
107
+ end
108
+
89
109
  it "duplicates the stream if it is a string" do
90
110
  src_obj = @source.add({}, stream: 'data')
91
111
  dst_obj = @importer.import(src_obj)
@@ -106,10 +126,11 @@ describe HexaPDF::Importer do
106
126
  assert_equal(90, page[:Rotate])
107
127
  end
108
128
 
109
- it "raise an error if the given object doesn't belong to the source document" do
129
+ it "works for importing objects from different documents" do
110
130
  other_doc = HexaPDF::Document.new
111
131
  other_obj = other_doc.add("test")
112
- assert_raises(HexaPDF::Error) { @importer.import(other_obj) }
132
+ imported = @importer.import(other_obj)
133
+ assert_equal("test", imported.value)
113
134
  end
114
135
  end
115
136
  end
@@ -54,6 +54,23 @@ describe HexaPDF::Parser do
54
54
  @parser = HexaPDF::Parser.new(@parse_io, @document)
55
55
  end
56
56
 
57
+ describe "linearized?" do
58
+ it "can determine whether a document is linearized" do
59
+ create_parser("%PDF-1.7\n%abcdefgh\n1 0 obj\n<</Linearized 1/H [2 4]/O 1/E 1/N 1/T 1>>\nendobj")
60
+ assert(@parser.linearized?)
61
+ end
62
+
63
+ it "returns false if the first object is not a linearization dictionary" do
64
+ create_parser("%PDF-1.7\n%abcdefgh\n1 0 obj\n<</Length 2 0 R>>\nstream\nhallo\nendstream\nendobj")
65
+ refute(@parser.linearized?)
66
+ end
67
+
68
+ it "returns false if there is a parse error" do
69
+ create_parser("%PDF-1.7\n%abcdefgh\n1 a obj thing")
70
+ refute(@parser.linearized?)
71
+ end
72
+ end
73
+
57
74
  describe "parse_indirect_object" do
58
75
  it "reads indirect objects sequentially" do
59
76
  object, oid, gen, stream = @parser.parse_indirect_object
@@ -108,7 +125,7 @@ describe HexaPDF::Parser do
108
125
  end
109
126
 
110
127
  it "treats indirect objects with invalid values as null objects" do
111
- create_parser("1 0 obj <</test ( /other (end)>> endobj")
128
+ create_parser("1 0 obj <</test <end)> > endobj")
112
129
  object, * = @parser.parse_indirect_object
113
130
  assert_nil(object)
114
131
  end
@@ -210,7 +227,7 @@ describe HexaPDF::Parser do
210
227
  end
211
228
 
212
229
  it "fails for invalid values" do
213
- create_parser("1 0 obj <</test ( /other (end)>> endobj")
230
+ create_parser("1 0 obj <</test <end)> >endobj")
214
231
  exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.parse_indirect_object }
215
232
  assert_match(/Invalid value after '1 0 obj'/, exp.message)
216
233
  end
@@ -199,6 +199,14 @@ describe HexaPDF::Revision do
199
199
  deleted = @rev.object(6)
200
200
  @rev.delete(6)
201
201
  assert_equal([obj, @obj, deleted], @rev.each_modified_object.to_a)
202
+ assert_same(obj, @rev.object(3))
203
+ end
204
+
205
+ it "optionally deletes the modified objects from the revision" do
206
+ obj = @rev.object(3)
207
+ obj.value = :other
208
+ assert_equal([obj], @rev.each_modified_object(delete: true).to_a)
209
+ refute_same(obj, @rev.object(3))
202
210
  end
203
211
 
204
212
  it "ignores object and xref streams that were deleted" do
@@ -207,6 +215,13 @@ describe HexaPDF::Revision do
207
215
  assert_equal([], @rev.each_modified_object.to_a)
208
216
  end
209
217
 
218
+ it "handles object and xref streams that were added appropriately depending on the 'all' arg" do
219
+ xref = @rev.add(HexaPDF::Dictionary.new({Type: :XRef}, oid: 8))
220
+ objstm = @rev.add(HexaPDF::Dictionary.new({Type: :ObjStm}, oid: 9))
221
+ assert_equal([], @rev.each_modified_object.to_a)
222
+ assert_equal([xref, objstm], @rev.each_modified_object(all: true).to_a)
223
+ end
224
+
210
225
  it "doesn't return non-modified objects" do
211
226
  @rev.object(2)
212
227
  assert_equal([], @rev.each_modified_object.to_a)
@@ -230,18 +245,4 @@ describe HexaPDF::Revision do
230
245
  assert_equal([], @rev.each_modified_object.to_a)
231
246
  end
232
247
  end
233
-
234
- describe "reset_objects" do
235
- it "deletes loaded objects" do
236
- @rev.object(2)
237
- @rev.reset_objects
238
- assert(@rev.instance_variable_get(:@objects).oids.empty?)
239
- end
240
-
241
- it "deletes added objects" do
242
- @rev.add(@obj)
243
- @rev.reset_objects
244
- assert(@rev.instance_variable_get(:@objects).oids.empty?)
245
- end
246
- end
247
248
  end
@@ -273,40 +273,48 @@ describe HexaPDF::Revisions do
273
273
  1 0 obj
274
274
  10
275
275
  endobj
276
+ xref
277
+ 0 2
278
+ 0000000000 65535 f
279
+ 0000000009 00000 n
280
+ trailer
281
+ << /Size 2 >>
282
+ startxref
283
+ 27
284
+ %%EOF
276
285
 
277
286
  2 0 obj
278
287
  20
279
288
  endobj
280
289
 
281
290
  3 0 obj
282
- << /Type /XRef /Size 3 /Index [2 1] /W [1 1 1] /Filter /ASCIIHexDecode /Length 6
291
+ << /Type /XRef /Size 4 /Index [2 1] /W [1 1 1] /Filter /ASCIIHexDecode /Length 6
283
292
  >>stream
284
- 011C00
293
+ 017600
285
294
  endstream
286
295
  endobj
287
296
 
288
297
  xref
289
- 0 4
290
- 0000000000 65535 f
291
- 0000000009 00000 n
298
+ 2 2
292
299
  0000000000 65535 f
293
- 0000000047 00000 n
300
+ 0000000137 00000 n
294
301
  trailer
295
- << /Size 3 >>
302
+ << /Size 4 /Prev 27>>
296
303
  startxref
297
- 170
304
+ 260
298
305
  %%EOF
299
306
 
300
307
  xref
301
308
  0 0
302
309
  trailer
303
- << /Size 3 /Prev 170 /XRefStm 47>>
310
+ << /Size 4 /Prev 260 /XRefStm 137>>
304
311
  startxref
305
- 302
312
+ 360
306
313
  %%EOF
307
314
  EOF
308
- doc = HexaPDF::Document.new(io: io)
309
- assert_equal(1, doc.revisions.count)
315
+ doc = HexaPDF::Document.new(io: io, config: {'parser.try_xref_reconstruction' => false})
316
+ assert_equal(2, doc.revisions.count)
317
+ assert_equal(10, doc.object(1).value)
310
318
  assert_equal(20, doc.object(2).value)
311
319
  end
312
320
 
@@ -348,4 +356,47 @@ describe HexaPDF::Revisions do
348
356
  HexaPDF::Document.new(io: io, config: {'parser.try_xref_reconstruction' => false})
349
357
  end
350
358
  end
359
+
360
+ it "merges the two revisions of a linearized PDF into one" do
361
+ io = StringIO.new(<<~EOF)
362
+ %PDF-1.2
363
+ 5 0 obj
364
+ <</Linearized 1>>
365
+ endobj
366
+ xref
367
+ 5 1
368
+ 0000000009 00000 n
369
+ trailer
370
+ <</ID[(a)(b)]/Info 1 0 R/Root 2 0 R/Size 6/Prev 394>>
371
+ %
372
+ 1 0 obj
373
+ <</ModDate(D:20221205233910+01'00')/Producer(HexaPDF version 0.27.0)>>
374
+ endobj
375
+ 2 0 obj
376
+ <</Type/Catalog/Pages 3 0 R>>
377
+ endobj
378
+ 3 0 obj
379
+ <</Type/Pages/Kids[4 0 R]/Count 1>>
380
+ endobj
381
+ 4 0 obj
382
+ <</Type/Page/MediaBox[0 0 595 842]/Parent 3 0 R/Resources<<>>>>
383
+ endobj
384
+ xref
385
+ 0 5
386
+ 0000000000 65535 f
387
+ 0000000133 00000 n
388
+ 0000000219 00000 n
389
+ 0000000264 00000 n
390
+ 0000000315 00000 n
391
+ trailer
392
+ <</ID[(a)(b)]/Info 1 0 R/Root 2 0 R/Size 5>>
393
+ startxref
394
+ 41
395
+ %%EOF
396
+ EOF
397
+ doc = HexaPDF::Document.new(io: io, config: {'parser.try_xref_reconstruction' => false})
398
+ assert(doc.revisions.parser.linearized?)
399
+ assert_equal(1, doc.revisions.count)
400
+ assert_same(5, doc.revisions.current.xref_section.max_oid)
401
+ end
351
402
  end
@@ -142,7 +142,7 @@ describe HexaPDF::Stream do
142
142
  def encoded_data(str, encoders = [])
143
143
  map = @document.config['filter.map']
144
144
  tmp = feeder(str)
145
- encoders.each {|e| tmp = ::Object.const_get(map[e]).encoder(tmp) }
145
+ encoders.each {|e| tmp = Object.const_get(map[e]).encoder(tmp) }
146
146
  collector(tmp)
147
147
  end
148
148
 
@@ -13,9 +13,18 @@ describe HexaPDF::Tokenizer do
13
13
  end
14
14
 
15
15
  it "handles object references" do
16
- create_tokenizer("1 0 R 2 15 R ")
16
+ #HexaPDF::Reference.new(1, 0), HexaPDF::Reference.new(1, 2), 2, -1, 'R', 0, 0, 'R', -1, 0, 'R',
17
+ create_tokenizer("1 0 R +2 +15 R 2 -1 R 0 0 R 0 10 R -1 0 R")
17
18
  assert_equal(HexaPDF::Reference.new(1, 0), @tokenizer.next_token)
18
19
  assert_equal(HexaPDF::Reference.new(2, 15), @tokenizer.next_token)
20
+ assert_equal(2, @tokenizer.next_token)
21
+ assert_equal(-1, @tokenizer.next_token)
22
+ assert_equal('R', @tokenizer.next_token)
23
+ assert_nil(@tokenizer.next_token)
24
+ assert_nil(@tokenizer.next_token)
25
+ assert_equal(-1, @tokenizer.next_token)
26
+ assert_equal(0, @tokenizer.next_token)
27
+ assert_equal('R', @tokenizer.next_token)
19
28
  @tokenizer.pos = 0
20
29
  assert_equal(HexaPDF::Reference.new(1, 0), @tokenizer.next_object)
21
30
  assert_equal(HexaPDF::Reference.new(2, 15), @tokenizer.next_object)
@@ -40,7 +40,7 @@ describe HexaPDF::Writer do
40
40
  219
41
41
  %%EOF
42
42
  3 0 obj
43
- <</Producer(HexaPDF version 0.26.2)>>
43
+ <</Producer(HexaPDF version 0.28.0)>>
44
44
  endobj
45
45
  xref
46
46
  3 1
@@ -72,7 +72,7 @@ describe HexaPDF::Writer do
72
72
  141
73
73
  %%EOF
74
74
  6 0 obj
75
- <</Producer(HexaPDF version 0.26.2)>>
75
+ <</Producer(HexaPDF version 0.28.0)>>
76
76
  endobj
77
77
  2 0 obj
78
78
  <</Length 10>>stream
@@ -125,6 +125,14 @@ describe HexaPDF::Writer do
125
125
  refute_match(/^trailer/, output_io.string)
126
126
  end
127
127
 
128
+ it "updates the PDF version using the catalog's /Version entry if necessary" do
129
+ doc = HexaPDF::Document.new(io: @std_input_io)
130
+ doc.version = '2.0'
131
+ output_io = StringIO.new
132
+ HexaPDF::Writer.write(doc, output_io, incremental: true)
133
+ assert_equal('2.0', HexaPDF::Document.new(io: output_io).version)
134
+ end
135
+
128
136
  it "raises an error if the used encryption was changed" do
129
137
  io = StringIO.new
130
138
  doc = HexaPDF::Document.new
@@ -206,7 +214,7 @@ describe HexaPDF::Writer do
206
214
  <</Type/Page/MediaBox[0 0 595 842]/Parent 2 0 R/Resources<<>>>>
207
215
  endobj
208
216
  5 0 obj
209
- <</Producer(HexaPDF version 0.26.2)>>
217
+ <</Producer(HexaPDF version 0.28.0)>>
210
218
  endobj
211
219
  4 0 obj
212
220
  <</Root 1 0 R/Info 5 0 R/Size 6/Type/XRef/W[1 1 2]/Index[0 6]/Filter/FlateDecode/DecodeParms<</Columns 4/Predictor 12>>/Length 33>>stream
@@ -18,12 +18,6 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
18
18
  @generator = HexaPDF::Type::AcroForm::AppearanceGenerator.new(@widget)
19
19
  end
20
20
 
21
- it "fails for unsupported button fields" do
22
- @field.flag(:push_button)
23
- @generator = HexaPDF::Type::AcroForm::AppearanceGenerator.new(@widget)
24
- assert_raises(HexaPDF::Error) { @generator.create_appearances }
25
- end
26
-
27
21
  it "fails for unsupported field types" do
28
22
  @field[:FT] = :Unknown
29
23
  assert_raises(HexaPDF::Error) { @generator.create_appearances }
@@ -141,8 +135,8 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
141
135
  end
142
136
 
143
137
  def execute
144
- @generator.send(:draw_marker, @xform.canvas, @widget[:Rect], @widget.border_style.width,
145
- @widget.marker_style)
138
+ @generator.send(:draw_marker, @xform.canvas, @widget[:Rect].width, @widget[:Rect].height,
139
+ @widget.border_style.width, @widget.marker_style)
146
140
  end
147
141
 
148
142
  it "handles the marker :circle specially for radio button widgets" do
@@ -196,6 +190,18 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
196
190
  [:show_text, ["4"]],
197
191
  [:end_text]])
198
192
  end
193
+
194
+ it "draws the default marker if an empty string is specified as marker" do
195
+ @widget.marker_style(style: '', color: 0.5, size: 5)
196
+ execute
197
+ assert_operators(@xform.stream,
198
+ [[:set_font_and_size, [:F1, 5]],
199
+ [:set_device_gray_non_stroking_color, [0.5]],
200
+ [:begin_text],
201
+ [:set_text_matrix, [1, 0, 0, 1, 2.885, 8.2725]],
202
+ [:show_text, ["4"]],
203
+ [:end_text]])
204
+ end
199
205
  end
200
206
  end
201
207
 
@@ -213,7 +219,15 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
213
219
 
214
220
  it "updates the widgets' /AS entry to point to the selected appearance" do
215
221
  @generator.create_appearances
216
- assert_equal(@field[:V], @widget[:AS])
222
+ assert_equal(:Off, @widget[:AS])
223
+
224
+ @field.field_value = :Yes
225
+ @generator.create_appearances
226
+ assert_equal(:Yes, @widget[:AS])
227
+
228
+ @field.delete(:V)
229
+ @generator.create_appearances
230
+ assert_equal(:Off, @widget[:AS])
217
231
  end
218
232
 
219
233
  it "set the print flag on the widgets" do
@@ -231,6 +245,33 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
231
245
  assert_equal(12, @widget[:Rect].height)
232
246
  end
233
247
 
248
+ describe "takes the rotation into account" do
249
+ def check_rotation(angle, width, height, matrix)
250
+ @widget[:MK] = {R: angle}
251
+ @field[:V] = :Yes
252
+ @generator.create_appearances
253
+ form = @widget[:AP][:N][@widget[:AS]]
254
+ assert_equal([0, 0, width, height], form[:BBox].value)
255
+ assert_equal(matrix, form[:Matrix].value)
256
+ end
257
+
258
+ it "works for 0 degrees" do
259
+ check_rotation(-360, @widget[:Rect].width, @widget[:Rect].height, [1, 0, 0, 1, 0, 0])
260
+ end
261
+
262
+ it "works for 90 degrees" do
263
+ check_rotation(450, @widget[:Rect].height, @widget[:Rect].width, [0, 1, -1, 0, 0, 0])
264
+ end
265
+
266
+ it "works for 180 degrees" do
267
+ check_rotation(180, @widget[:Rect].width, @widget[:Rect].height, [0, -1, -1, 0, 0, 0])
268
+ end
269
+
270
+ it "works for 270 degrees" do
271
+ check_rotation(-90, @widget[:Rect].height, @widget[:Rect].width, [0, -1, 1, 0, 0, 0])
272
+ end
273
+ end
274
+
234
275
  it "creates the needed appearance streams" do
235
276
  @widget[:AP][:N].delete(:Off)
236
277
  @generator.create_appearances
@@ -292,61 +333,21 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
292
333
  assert_equal(:Off, @widget[:AS])
293
334
  end
294
335
 
295
- it "set the print flag on the widgets" do
296
- @generator.create_appearances
297
- assert(@widget.flagged?(:print))
298
- end
299
-
300
- it "adjusts the /Rect if width is zero" do
301
- @generator.create_appearances
302
- assert_equal(12, @widget[:Rect].width)
303
- end
304
-
305
- it "adjusts the /Rect if height is zero" do
306
- @generator.create_appearances
307
- assert_equal(12, @widget[:Rect].height)
308
- end
309
-
310
336
  it "creates the needed appearance streams" do
311
337
  @generator.create_appearances
312
338
  assert_equal(:XObject, @widget[:AP][:N][:Off].type)
313
339
  assert_equal(:XObject, @widget[:AP][:N][:radio].type)
314
340
  end
341
+ end
315
342
 
316
- it "creates the /Off appearance stream" do
317
- @widget.marker_style(style: :cross)
318
- @generator.create_appearances
319
- assert_operators(@widget[:AP][:N][:Off].stream,
320
- [[:save_graphics_state],
321
- [:set_device_gray_non_stroking_color, [1.0]],
322
- [:append_rectangle, [0, 0, 12, 12]],
323
- [:fill_path_non_zero],
324
- [:append_rectangle, [0.5, 0.5, 11, 11]],
325
- [:stroke_path], [:restore_graphics_state]])
326
- end
327
-
328
- it "creates the appearance stream according to the set value" do
329
- @widget.marker_style(style: :check)
330
- @generator.create_appearances
331
- assert_operators(@widget[:AP][:N][:radio].stream,
332
- [[:save_graphics_state],
333
- [:set_device_gray_non_stroking_color, [1.0]],
334
- [:append_rectangle, [0, 0, 12, 12]],
335
- [:fill_path_non_zero],
336
- [:append_rectangle, [0.5, 0.5, 11, 11]],
337
- [:stroke_path], [:restore_graphics_state],
338
-
339
- [:save_graphics_state],
340
- [:set_font_and_size, [:F1, 10]],
341
- [:begin_text],
342
- [:set_text_matrix, [1, 0, 0, 1, 1.77, 2.545]],
343
- [:show_text, ["4"]],
344
- [:end_text],
345
- [:restore_graphics_state]])
343
+ describe "push buttons" do
344
+ before do
345
+ @field.initialize_as_push_button
346
+ @widget = @field.create_widget(@page, Rect: [0, 0, 0, 0])
347
+ @generator = HexaPDF::Type::AcroForm::AppearanceGenerator.new(@widget)
346
348
  end
347
349
 
348
- it "fails if the appearance dictionaries are not set up" do
349
- @widget[:AP][:N].delete(:radio)
350
+ it "fails because it is not implemented yet" do
350
351
  assert_raises(HexaPDF::Error) { @generator.create_appearances }
351
352
  end
352
353
  end
@@ -417,6 +418,37 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
417
418
  assert_match(/test1/, form.contents)
418
419
  end
419
420
 
421
+ describe "takes the rotation into account" do
422
+ before do
423
+ @widget[:Rect] = [0, 0, 100, 20]
424
+ end
425
+
426
+ def check_rotation(angle, width, height, matrix)
427
+ @widget[:MK] = {R: angle}
428
+ @field[:V] = 'test'
429
+ @generator.create_appearances
430
+ form = @widget[:AP][:N]
431
+ assert_equal([0, 0, width, height], form[:BBox].value)
432
+ assert_equal(matrix, form[:Matrix].value)
433
+ end
434
+
435
+ it "works for 0 degrees" do
436
+ check_rotation(-360, @widget[:Rect].width, @widget[:Rect].height, [1, 0, 0, 1, 0, 0])
437
+ end
438
+
439
+ it "works for 90 degrees" do
440
+ check_rotation(450, @widget[:Rect].height, @widget[:Rect].width, [0, 1, -1, 0, 0, 0])
441
+ end
442
+
443
+ it "works for 180 degrees" do
444
+ check_rotation(180, @widget[:Rect].width, @widget[:Rect].height, [0, -1, -1, 0, 0, 0])
445
+ end
446
+
447
+ it "works for 270 degrees" do
448
+ check_rotation(-90, @widget[:Rect].height, @widget[:Rect].width, [0, -1, 1, 0, 0, 0])
449
+ end
450
+ end
451
+
420
452
  describe "font size calculation" do
421
453
  before do
422
454
  @widget[:Rect].height = 20
@@ -496,6 +528,53 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
496
528
  end
497
529
  end
498
530
 
531
+ describe "Javascript action AFNumber_Format" do
532
+ before do
533
+ @field.field_value = '1234567.898765'
534
+ @action = {S: :JavaScript, JS: ''}
535
+ @field[:AA] = {F: @action}
536
+ end
537
+
538
+ def assert_format(arg_string, result, range)
539
+ @action[:JS] = "AFNumber_Format(#{arg_string});"
540
+ @generator.create_appearances
541
+ assert_operators(@widget[:AP][:N].stream, result, range: range)
542
+ end
543
+
544
+ it "respects the set number of decimals" do
545
+ assert_format('0, 2, 0, 0, "E", false',
546
+ [:show_text, ["1.234.568E"]], 9)
547
+ assert_format('2, 2, 0, 0, "E", false',
548
+ [:show_text, ["1.234.567,90E"]], 9)
549
+ end
550
+
551
+ it "respects the digit separator style" do
552
+ ["1,234,567.90", "1234567.90", "1.234.567,90", "1234567,90"].each_with_index do |result, style|
553
+ assert_format("2, #{style}, 0, 0, \"\", false", [:show_text, [result]], 9)
554
+ end
555
+ end
556
+
557
+ it "respects the negative value styling" do
558
+ @field.field_value = '-1234567.898'
559
+ ["-E1234567,90", "E1234567,90", "(E1234567,90)", "(E1234567,90)"].each_with_index do |result, style|
560
+ assert_format("2, 3, #{style}, 0, \"E\", true",
561
+ [[:set_device_rgb_non_stroking_color, [style % 2, 0.0, 0.0]],
562
+ [:begin_text],
563
+ [:set_text_matrix, [1, 0, 0, 1, 2, 3.240724]],
564
+ [:show_text, [result]]], 6..9)
565
+ end
566
+ end
567
+
568
+ it "respects the specified currency string and position" do
569
+ assert_format('2, 3, 0, 0, " E", false', [:show_text, ["1234567,90 E"]], 9)
570
+ assert_format('2, 3, 0, 0, "E ", true', [:show_text, ["E 1234567,90"]], 9)
571
+ end
572
+
573
+ it "does nothing to the value if the Javascript method could not be determined " do
574
+ assert_format('2, 3, 0, 0, " E", false, a', [:show_text, ["1234567.898765"]], 8)
575
+ end
576
+ end
577
+
499
578
  it "creates the /N appearance stream according to the set string" do
500
579
  @field.field_value = 'Text'
501
580
  @field.set_default_appearance_string(font_color: "red")