hexapdf 0.26.2 → 0.28.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 (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")