hexapdf 0.46.0 → 1.0.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 (87) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +83 -16
  3. data/lib/hexapdf/composer.rb +7 -0
  4. data/lib/hexapdf/configuration.rb +13 -0
  5. data/lib/hexapdf/content/parser.rb +3 -1
  6. data/lib/hexapdf/digital_signature/cms_handler.rb +13 -0
  7. data/lib/hexapdf/digital_signature/signature.rb +1 -1
  8. data/lib/hexapdf/digital_signature/signing/default_handler.rb +1 -0
  9. data/lib/hexapdf/document.rb +14 -3
  10. data/lib/hexapdf/encryption/standard_security_handler.rb +32 -26
  11. data/lib/hexapdf/font/cmap/writer.rb +58 -4
  12. data/lib/hexapdf/font/cmap.rb +7 -0
  13. data/lib/hexapdf/font/true_type_wrapper.rb +41 -16
  14. data/lib/hexapdf/importer.rb +1 -1
  15. data/lib/hexapdf/layout/table_box.rb +57 -10
  16. data/lib/hexapdf/layout/text_fragment.rb +2 -1
  17. data/lib/hexapdf/object.rb +1 -1
  18. data/lib/hexapdf/parser.rb +1 -1
  19. data/lib/hexapdf/reference.rb +1 -1
  20. data/lib/hexapdf/task/merge_acro_form.rb +164 -0
  21. data/lib/hexapdf/task/optimize.rb +4 -4
  22. data/lib/hexapdf/task.rb +1 -0
  23. data/lib/hexapdf/tokenizer.rb +2 -0
  24. data/lib/hexapdf/type/acro_form/appearance_generator.rb +8 -4
  25. data/lib/hexapdf/type/acro_form/form.rb +14 -24
  26. data/lib/hexapdf/type/acro_form/signature_field.rb +18 -7
  27. data/lib/hexapdf/type/acro_form/variable_text_field.rb +12 -4
  28. data/lib/hexapdf/type/actions/go_to.rb +1 -0
  29. data/lib/hexapdf/type/actions/go_to_r.rb +1 -0
  30. data/lib/hexapdf/type/actions/launch.rb +5 -1
  31. data/lib/hexapdf/type/annotation.rb +6 -1
  32. data/lib/hexapdf/type/annotations/markup_annotation.rb +14 -1
  33. data/lib/hexapdf/type/annotations/widget.rb +4 -2
  34. data/lib/hexapdf/type/catalog.rb +3 -0
  35. data/lib/hexapdf/type/cid_font.rb +4 -1
  36. data/lib/hexapdf/type/file_specification.rb +17 -14
  37. data/lib/hexapdf/type/font_descriptor.rb +4 -3
  38. data/lib/hexapdf/type/font_simple.rb +3 -1
  39. data/lib/hexapdf/type/font_true_type.rb +2 -0
  40. data/lib/hexapdf/type/font_type0.rb +1 -1
  41. data/lib/hexapdf/type/font_type1.rb +7 -0
  42. data/lib/hexapdf/type/font_type3.rb +0 -1
  43. data/lib/hexapdf/type/form.rb +5 -2
  44. data/lib/hexapdf/type/graphics_state_parameter.rb +7 -4
  45. data/lib/hexapdf/type/image.rb +8 -4
  46. data/lib/hexapdf/type/info.rb +2 -2
  47. data/lib/hexapdf/type/mark_information.rb +2 -2
  48. data/lib/hexapdf/type/optional_content_configuration.rb +1 -1
  49. data/lib/hexapdf/type/optional_content_membership.rb +1 -1
  50. data/lib/hexapdf/type/page.rb +5 -3
  51. data/lib/hexapdf/type/resources.rb +6 -6
  52. data/lib/hexapdf/type/viewer_preferences.rb +4 -3
  53. data/lib/hexapdf/version.rb +1 -1
  54. data/lib/hexapdf/writer.rb +1 -0
  55. data/test/data/standard-security-handler/bothpwd-aes-256bit-V5-R5.pdf +43 -0
  56. data/test/data/standard-security-handler/nopwd-aes-256bit-V5-R5.pdf +44 -0
  57. data/test/data/standard-security-handler/ownerpwd-aes-256bit-V5-R5.pdf +43 -0
  58. data/test/data/standard-security-handler/userpwd-aes-256bit-V5-R5.pdf +0 -0
  59. data/test/hexapdf/common_tokenizer_tests.rb +5 -0
  60. data/test/hexapdf/digital_signature/signing/test_default_handler.rb +6 -0
  61. data/test/hexapdf/digital_signature/test_cms_handler.rb +12 -7
  62. data/test/hexapdf/digital_signature/test_signature.rb +7 -0
  63. data/test/hexapdf/digital_signature/test_signatures.rb +12 -7
  64. data/test/hexapdf/encryption/test_standard_security_handler.rb +5 -2
  65. data/test/hexapdf/font/cmap/test_writer.rb +73 -16
  66. data/test/hexapdf/font/test_true_type_wrapper.rb +17 -3
  67. data/test/hexapdf/layout/test_list_box.rb +7 -7
  68. data/test/hexapdf/layout/test_table_box.rb +52 -0
  69. data/test/hexapdf/layout/test_text_fragment.rb +3 -3
  70. data/test/hexapdf/layout/test_text_layouter.rb +4 -2
  71. data/test/hexapdf/task/test_merge_acro_form.rb +104 -0
  72. data/test/hexapdf/task/test_optimize.rb +2 -0
  73. data/test/hexapdf/test_composer.rb +8 -0
  74. data/test/hexapdf/test_document.rb +12 -3
  75. data/test/hexapdf/test_importer.rb +7 -0
  76. data/test/hexapdf/test_parser.rb +7 -0
  77. data/test/hexapdf/test_writer.rb +19 -5
  78. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +40 -23
  79. data/test/hexapdf/type/acro_form/test_form.rb +7 -8
  80. data/test/hexapdf/type/acro_form/test_signature_field.rb +3 -1
  81. data/test/hexapdf/type/acro_form/test_variable_text_field.rb +14 -1
  82. data/test/hexapdf/type/actions/test_launch.rb +6 -2
  83. data/test/hexapdf/type/annotations/test_widget.rb +4 -0
  84. data/test/hexapdf/type/test_font_type1.rb +5 -0
  85. data/test/hexapdf/type/test_form.rb +1 -1
  86. data/test/hexapdf/type/test_page.rb +7 -1
  87. metadata +8 -2
@@ -0,0 +1,104 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'test_helper'
4
+ require 'hexapdf/document'
5
+ require 'hexapdf/task/merge_acro_form'
6
+
7
+ describe HexaPDF::Task::MergeAcroForm do
8
+ before do
9
+ @doc = HexaPDF::Document.new
10
+ @doc.pages.add
11
+ @doc.pages.add
12
+ form = @doc.acro_form(create: true)
13
+ field = form.create_text_field("Text")
14
+ field.create_widget(@doc.pages[0], Rect: [0, 0, 0, 0])
15
+ field.create_widget(@doc.pages[1], Rect: [0, 0, 0, 0])
16
+
17
+ form.create_text_field("Calc.Field a")
18
+ form.create_text_field("Calc.Field b")
19
+ field = form.create_text_field('Other.Calculation 1')
20
+ field.set_calculate_action(:sum, fields: ["Calc.Field a", "Calc.Field b"])
21
+ field.create_widget(@doc.pages[1])
22
+ field = form.create_text_field('Other.Calculation 2')
23
+ field.set_calculate_action(:sfn, fields: "Calc.Field\\ a + Calc.Field\\ b")
24
+ field.create_widget(@doc.pages[1])
25
+
26
+ @root_fields = @doc.acro_form.root_fields
27
+
28
+ @doc.dispatch_message(:complete_objects)
29
+ @doc.validate
30
+ @doc1 = @doc.duplicate
31
+ @pages = []
32
+ @pages << @doc.pages.add(@doc.import(@doc1.pages[0]))
33
+ @pages << @doc.pages.add(@doc.import(@doc1.pages[1]))
34
+ end
35
+
36
+ it "selects a unique name for the root field" do
37
+ @doc.task(:merge_acro_form, source: @doc1, pages: @pages)
38
+ assert_equal('merged_1', @root_fields[3][:T])
39
+
40
+ @root_fields << @doc.wrap({T: 'merged_23'})
41
+ @doc.task(:merge_acro_form, source: @doc1, pages: @pages)
42
+ assert_equal('merged_24', @root_fields[5][:T])
43
+ end
44
+
45
+ it "merges the /DR entry of the main AcroForm dictionary" do
46
+ @doc.task(:merge_acro_form, source: @doc1, pages: @pages)
47
+ assert(@doc.acro_form.default_resources[:Font].key?(:F2))
48
+ end
49
+
50
+ it "updates the /SigFlags if necessary" do
51
+ @doc.task(:merge_acro_form, source: @doc1, pages: [@pages[0]])
52
+ refute(@doc.acro_form.signature_flag?(:signatures_exist))
53
+
54
+ @pages[0][:Annots][0].form_field[:FT] = :Sig
55
+ @doc.task(:merge_acro_form, source: @doc1, pages: [@pages[0]])
56
+ refute(@doc.acro_form.signature_flag?(:signatures_exist))
57
+
58
+ @doc1.acro_form.signature_flag(:signatures_exist)
59
+ @doc.task(:merge_acro_form, source: @doc1, pages: [@pages[0]])
60
+ assert(@doc.acro_form.signature_flag?(:signatures_exist))
61
+ end
62
+
63
+ it "applies the /DA and /Q entries of the source AcroForm to the created root field" do
64
+ @doc1.acro_form.set_default_appearance_string
65
+ @doc1.acro_form[:Q] = @doc1.add(5)
66
+ @doc.task(:merge_acro_form, source: @doc1, pages: [@pages[0]])
67
+ assert_equal('0.0 g /F2 0 Tf', @root_fields[3][:DA])
68
+ assert_equal(5, @root_fields[3][:Q])
69
+ end
70
+
71
+ it "merges only the fields references in the given pages" do
72
+ @doc.task(:merge_acro_form, source: @doc1, pages: [@pages[0]])
73
+ assert_equal('merged_1', @root_fields[3][:T])
74
+ assert_equal(1, @root_fields[3][:Kids].size)
75
+ end
76
+
77
+ it "only merges fields that have at least one widget" do
78
+ @doc.task(:merge_acro_form, source: @doc1, pages: @pages)
79
+ assert_equal(2, @root_fields[3][:Kids].size)
80
+ assert_nil(@doc.acro_form.field_by_name('merged_1.Calc'))
81
+ end
82
+
83
+ it "updates the /DA entries of widgets and fields" do
84
+ @pages[0][:Annots][0][:DA] = '/F1 10 Tf'
85
+ @doc.task(:merge_acro_form, source: @doc1, pages: @pages)
86
+ field = @doc.acro_form.field_by_name('merged_1.Text')
87
+ assert_equal('0.0 g /F2 0 Tf', field[:DA])
88
+ assert_equal('/F2 10 Tf', field.each_widget.to_a[0][:DA])
89
+ end
90
+
91
+ it "doesn't update the calculation actions if no field with one is merged" do
92
+ @doc.task(:merge_acro_form, source: @doc1, pages: [@pages[0]])
93
+ assert_equal(2, @doc.acro_form[:CO].size)
94
+ end
95
+
96
+ it "updates the field names in known calculation actions" do
97
+ @doc.task(:merge_acro_form, source: @doc1, pages: @pages)
98
+ assert_equal(4, @doc.acro_form[:CO].size)
99
+ js = @doc.acro_form.field_by_name('merged_1.Other.Calculation 1')[:AA][:C][:JS]
100
+ assert_match(/merged_1.Calc.Field a/, js)
101
+ js = @doc.acro_form.field_by_name('merged_1.Other.Calculation 2')[:AA][:C][:JS]
102
+ assert_match(/merged_1.Calc.Field a/, js)
103
+ end
104
+ end
@@ -8,6 +8,7 @@ describe HexaPDF::Task::Optimize do
8
8
  class TestType < HexaPDF::Dictionary
9
9
 
10
10
  define_type :Test
11
+ define_field :Type, type: Symbol, default: type
11
12
  define_field :Optional, type: Symbol, default: :Optional
12
13
 
13
14
  end
@@ -46,6 +47,7 @@ describe HexaPDF::Task::Optimize do
46
47
  end
47
48
 
48
49
  def assert_default_deleted
50
+ assert(@doc.object(1).key?(:Type))
49
51
  refute(@doc.object(1).key?(:Optional))
50
52
  end
51
53
 
@@ -63,6 +63,14 @@ describe HexaPDF::Composer do
63
63
  end
64
64
  end
65
65
 
66
+ it "writes the document to a string" do
67
+ pdf = HexaPDF::Composer.new
68
+ pdf.new_page
69
+ str = pdf.write_to_string
70
+ doc = HexaPDF::Document.new(io: StringIO.new(str))
71
+ assert_equal(2, doc.pages.count)
72
+ end
73
+
66
74
  describe "new_page" do
67
75
  it "creates a new page" do
68
76
  c = HexaPDF::Composer.new(page_size: [0, 0, 50, 100], margin: 10)
@@ -497,10 +497,10 @@ describe HexaPDF::Document do
497
497
 
498
498
  it "returns all signature fields of the document" do
499
499
  form = @doc.acro_form(create: true)
500
- sig1 = @doc.add({FT: :Sig, T: 'sig1', V: :sig1})
501
- sig2 = @doc.add({FT: :Sig, T: 'sig2', V: :sig2})
500
+ sig1 = @doc.add({FT: :Sig, T: 'sig1', V: {k: :sig1}})
501
+ sig2 = @doc.add({FT: :Sig, T: 'sig2', V: {k: :sig2}})
502
502
  form.root_fields << sig1 << sig2
503
- assert_equal([:sig1, :sig2], @doc.signatures.to_a)
503
+ assert_equal([{k: :sig1}, {k: :sig2}], @doc.signatures.to_a)
504
504
  end
505
505
 
506
506
  it "allows to conveniently sign a document" do
@@ -595,4 +595,13 @@ describe HexaPDF::Document do
595
595
  refute(dupped.encrypted?)
596
596
  end
597
597
  end
598
+
599
+ it "writes the document to a string" do
600
+ doc = HexaPDF::Document.new
601
+ doc.trailer.info[:test] = :test
602
+ str = doc.write_to_string(update_fields: false)
603
+ assert_equal(Encoding::ASCII_8BIT, str.encoding)
604
+ doc = HexaPDF::Document.new(io: StringIO.new(str))
605
+ assert_equal(:test, doc.trailer.info[:test])
606
+ end
598
607
  end
@@ -147,6 +147,13 @@ describe HexaPDF::Importer do
147
147
  assert_nil(obj[:pages])
148
148
  end
149
149
 
150
+ it "handles null values correctly" do
151
+ @source.add(@hash)
152
+ @source.delete(@hash)
153
+ obj = @importer.import(@obj)
154
+ assert_nil(obj[:hash])
155
+ end
156
+
150
157
  it "imports Page objects correctly by copying the inherited values" do
151
158
  page = @importer.import(@source.pages[0])
152
159
  assert_equal(90, page[:Rotate])
@@ -152,6 +152,13 @@ describe HexaPDF::Parser do
152
152
  assert_equal('12', collector(stream.fiber))
153
153
  end
154
154
 
155
+ it "recovers from a non-existing indirect reference to a stream length value" do
156
+ create_parser("1 0 obj<</Length 2 0 R>> stream\n12(ab\nendstream endobj")
157
+ obj, _, _, stream = @parser.parse_indirect_object
158
+ assert_equal(5, obj[:Length])
159
+ assert_equal('12(ab', collector(stream.fiber))
160
+ end
161
+
155
162
  it "works even if the keyword endobj is missing or mangled" do
156
163
  create_parser("1 0 obj<</Length 4>>5")
157
164
  object, * = @parser.parse_indirect_object
@@ -48,10 +48,15 @@ describe HexaPDF::Writer do
48
48
  trailer
49
49
  <</Size 4/Root<</Type/Catalog>>/Info 3 0 R/Prev 219>>
50
50
  startxref
51
- 349
51
+ #{343 + HexaPDF::VERSION.length}
52
52
  %%EOF
53
53
  EOF
54
54
 
55
+ xref_stream = case HexaPDF::VERSION.length
56
+ when 5 then "x\xDAcbdlc``b`\xB0\x04\x93\x93\x19\x18\x00\f\x0F\x01["
57
+ when 6 then "x\xDAcbdlg``b`\xB0\x04\x93\x93\x18\x18\x00\f\e\x01["
58
+ else fail
59
+ end
55
60
  @compressed_input_io = StringIO.new(<<~EOF.force_encoding(Encoding::BINARY))
56
61
  %PDF-1.7
57
62
  %\xCF\xEC\xFF\xE8\xD7\xCB\xCD
@@ -64,7 +69,7 @@ describe HexaPDF::Writer do
64
69
  20
65
70
  endobj
66
71
  3 0 obj
67
- <</Type/XRef/Size 6/W[1 1 2]/Index[0 4 5 1]/Filter/FlateDecode/DecodeParms<</Columns 4/Predictor 12>>/Length 31>>stream
72
+ <</Size 6/Type/XRef/W[1 1 2]/Index[0 4 5 1]/Filter/FlateDecode/DecodeParms<</Columns 4/Predictor 12>>/Length 31>>stream
68
73
  x\xDAcb`\xF8\xFF\x9F\x89\x89\x95\x91\x91\xE9\x7F\x19\x03\x03\x13\x83\x10\x88he`\x00\x00B4\x04\x1E
69
74
  endstream
70
75
  endobj
@@ -80,12 +85,12 @@ describe HexaPDF::Writer do
80
85
  endstream
81
86
  endobj
82
87
  4 0 obj
83
- <</Type/XRef/Size 7/Root<</Type/Catalog>>/Info 6 0 R/Prev 141/W[1 2 2]/Index[2 1 4 1 6 1]/Filter/FlateDecode/DecodeParms<</Columns 5/Predictor 12>>/Length 22>>stream
84
- x\xDAcbdlg``b`\xB0\x04\x93\x93\x18\x18\x00\f\e\x01[
88
+ <</Size 7/Root<</Type/Catalog>>/Info 6 0 R/Prev 141/Type/XRef/W[1 2 2]/Index[2 1 4 1 6 1]/Filter/FlateDecode/DecodeParms<</Columns 5/Predictor 12>>/Length 22>>stream
89
+ #{xref_stream}
85
90
  endstream
86
91
  endobj
87
92
  startxref
88
- 448
93
+ #{442 + HexaPDF::VERSION.length}
89
94
  %%EOF
90
95
  EOF
91
96
  end
@@ -270,4 +275,13 @@ describe HexaPDF::Writer do
270
275
  doc = HexaPDF::Document.new(io: io)
271
276
  refute(doc.trailer.key?(:XRefStm))
272
277
  end
278
+
279
+ it "removes the /Type entry in a non-xref stream trailer" do
280
+ io = StringIO.new
281
+ doc = HexaPDF::Document.new
282
+ doc.trailer[:Type] = :XRef
283
+ doc.write(io)
284
+ doc = HexaPDF::Document.new(io: io)
285
+ refute(doc.trailer.key?(:Type))
286
+ end
273
287
  end
@@ -291,6 +291,22 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
291
291
  assert_equal(:XObject, @widget[:AP][:N][:Other].type)
292
292
  end
293
293
 
294
+ it "uses the field's value or :Yes for the on state if the appearance dictionary doesn't contain a name for it" do
295
+ @widget[:AP][:N].delete(:Yes)
296
+ @generator.create_appearances
297
+ assert_equal(:XObject, @widget[:AP][:N][:Yes].type)
298
+
299
+ @widget[:AP][:N].delete(:Yes)
300
+ @field[:V] = nil
301
+ @generator.create_appearances
302
+ assert_equal(:XObject, @widget[:AP][:N][:Yes].type)
303
+
304
+ @widget[:AP][:N].delete(:Yes)
305
+ @field[:V] = "other" # some PDFs use a string instead of the correct symbol
306
+ @generator.create_appearances
307
+ assert_equal(:XObject, @widget[:AP][:N][:other].type)
308
+ end
309
+
294
310
  it "creates the needed appearance streams" do
295
311
  @widget[:AP][:N].delete(:Off)
296
312
  @generator.create_appearances
@@ -327,11 +343,6 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
327
343
  [:end_text],
328
344
  [:restore_graphics_state]])
329
345
  end
330
-
331
- it "fails if the appearance dictionary doesn't contain a name for the on state" do
332
- @widget[:AP][:N].delete(:Yes)
333
- assert_raises(HexaPDF::Error) { @generator.create_appearances }
334
- end
335
346
  end
336
347
 
337
348
  describe "radio button" do
@@ -441,6 +452,12 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
441
452
  assert_equal(form, @widget[:AP][:N])
442
453
  refute(form.key?(:key))
443
454
  assert_match(/test1/, form.contents)
455
+
456
+ form.delete(:Type)
457
+ @widget[:AP][:N] = @doc.wrap(form, type: HexaPDF::Type::Annotation)
458
+ @field[:V] = 'test2'
459
+ @generator.create_appearances
460
+ assert_match(/test2/, form.contents)
444
461
  end
445
462
 
446
463
  describe "takes the rotation into account" do
@@ -532,7 +549,7 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
532
549
  @field.text_alignment(:left)
533
550
  @generator.create_appearances
534
551
  assert_operators(@widget[:AP][:N].stream,
535
- [:set_text_matrix, [1, 0, 0, 1, 2, 6.41]],
552
+ [:move_text, [2, 6.41]],
536
553
  range: 7)
537
554
  end
538
555
 
@@ -540,7 +557,7 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
540
557
  @field.text_alignment(:right)
541
558
  @generator.create_appearances
542
559
  assert_operators(@widget[:AP][:N].stream,
543
- [:set_text_matrix, [1, 0, 0, 1, 78.55, 6.41]],
560
+ [:move_text, [78.55, 6.41]],
544
561
  range: 7)
545
562
  end
546
563
 
@@ -548,7 +565,7 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
548
565
  @field.text_alignment(:center)
549
566
  @generator.create_appearances
550
567
  assert_operators(@widget[:AP][:N].stream,
551
- [:set_text_matrix, [1, 0, 0, 1, 40.275, 6.41]],
568
+ [:move_text, [40.275, 6.41]],
552
569
  range: 7)
553
570
  end
554
571
 
@@ -559,7 +576,7 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
559
576
 
560
577
  @generator.create_appearances
561
578
  assert_operators(@widget[:AP][:N].stream,
562
- [:set_text_matrix, [1, 0, 0, 1, 2, 6.41]],
579
+ [:move_text, [2, 6.41]],
563
580
  range: 7)
564
581
  ensure
565
582
  font_metrics.cap_height = cap_height
@@ -569,7 +586,7 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
569
586
  @widget[:Rect].height = 5
570
587
  @generator.create_appearances
571
588
  assert_operators(@widget[:AP][:N].stream,
572
- [:set_text_matrix, [1, 0, 0, 1, 2, 3.07]],
589
+ [:move_text, [2, 3.07]],
573
590
  range: 7)
574
591
  end
575
592
  end
@@ -597,7 +614,7 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
597
614
  [:set_font_and_size, [:F1, 10]],
598
615
  [:set_device_rgb_non_stroking_color, [1.0, 0.0, 0.0]],
599
616
  [:begin_text],
600
- [:set_text_matrix, [1, 0, 0, 1, 2, 2.035]],
617
+ [:move_text, [2, 2.035]],
601
618
  [:show_text, ["Te "]],
602
619
  [:set_font_and_size, [:F2, 10]],
603
620
  [:move_text, [14.45, 0]],
@@ -628,7 +645,7 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
628
645
  @field.text_alignment(:left)
629
646
  @generator.create_appearances
630
647
  assert_operators(@widget[:AP][:N].stream,
631
- [:set_text_matrix, [1, 0, 0, 1, 2, 16.195]],
648
+ [:move_text, [2, 16.195]],
632
649
  range: 9)
633
650
  end
634
651
 
@@ -636,7 +653,7 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
636
653
  @field.text_alignment(:right)
637
654
  @generator.create_appearances
638
655
  assert_operators(@widget[:AP][:N].stream,
639
- [:set_text_matrix, [1, 0, 0, 1, 78.55, 16.195]],
656
+ [:move_text, [78.55, 16.195]],
640
657
  range: 9)
641
658
  end
642
659
 
@@ -644,7 +661,7 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
644
661
  @field.text_alignment(:center)
645
662
  @generator.create_appearances
646
663
  assert_operators(@widget[:AP][:N].stream,
647
- [:set_text_matrix, [1, 0, 0, 1, 40.275, 16.195]],
664
+ [:move_text, [40.275, 16.195]],
648
665
  range: 9)
649
666
  end
650
667
  end
@@ -664,7 +681,7 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
664
681
  [:set_font_and_size, [:F1, 10]],
665
682
  [:set_device_rgb_non_stroking_color, [1.0, 0.0, 0.0]],
666
683
  [:begin_text],
667
- [:set_text_matrix, [1, 0, 0, 1, 2, 16.195]],
684
+ [:move_text, [2, 16.195]],
668
685
  [:show_text, ['Test']],
669
686
  [:move_text_next_line],
670
687
  [:show_text, ['Value']],
@@ -686,7 +703,7 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
686
703
  [:set_leading, [9.25]],
687
704
  [:set_font_and_size, [:F1, 8]],
688
705
  [:begin_text],
689
- [:set_text_matrix, [1, 0, 0, 1, 2, 18.556]],
706
+ [:move_text, [2, 18.556]],
690
707
  [:show_text, ['Test']],
691
708
  [:move_text_next_line],
692
709
  [:show_text, ['Test']],
@@ -717,7 +734,7 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
717
734
  @field.text_alignment(:left)
718
735
  @generator.create_appearances
719
736
  assert_operators(@widget[:AP][:N].stream,
720
- [:set_text_matrix, [1, 0, 0, 1, 2.945, 6.41]],
737
+ [:move_text, [2.945, 6.41]],
721
738
  range: 7)
722
739
  end
723
740
 
@@ -725,7 +742,7 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
725
742
  @field.text_alignment(:right)
726
743
  @generator.create_appearances
727
744
  assert_operators(@widget[:AP][:N].stream,
728
- [:set_text_matrix, [1, 0, 0, 1, 62.945, 6.41]],
745
+ [:move_text, [62.945, 6.41]],
729
746
  range: 7)
730
747
  end
731
748
 
@@ -733,7 +750,7 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
733
750
  @field.text_alignment(:center)
734
751
  @generator.create_appearances
735
752
  assert_operators(@widget[:AP][:N].stream,
736
- [:set_text_matrix, [1, 0, 0, 1, 32.945, 6.41]],
753
+ [:move_text, [32.945, 6.41]],
737
754
  range: 7)
738
755
  end
739
756
 
@@ -742,7 +759,7 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
742
759
  @field.text_alignment(:center)
743
760
  @generator.create_appearances
744
761
  assert_operators(@widget[:AP][:N].stream,
745
- [:set_text_matrix, [1, 0, 0, 1, 22.39, 6.41]],
762
+ [:move_text, [22.39, 6.41]],
746
763
  range: 7)
747
764
  end
748
765
  end
@@ -760,7 +777,7 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
760
777
  [:set_font_and_size, [:F1, 10]],
761
778
  [:set_device_rgb_non_stroking_color, [1.0, 0.0, 0.0]],
762
779
  [:begin_text],
763
- [:set_text_matrix, [1, 0, 0, 1, 2.945, 6.41]],
780
+ [:move_text, [2.945, 6.41]],
764
781
  [:show_text_with_positioning, [['T', -416.5, 'e', -472, 'x', -611, 't']]],
765
782
  [:end_text],
766
783
  [:restore_graphics_state],
@@ -772,7 +789,7 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
772
789
  @generator.create_appearances
773
790
  assert_operators(@widget[:AP][:N].stream,
774
791
  [[:begin_text],
775
- [:set_text_matrix, [1, 0, 0, 1, 2, 6.41]],
792
+ [:move_text, [2, 6.41]],
776
793
  [:end_text]], range: 6..8)
777
794
  end
778
795
 
@@ -853,7 +870,7 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
853
870
  [:set_font_and_size, [:F1, 12]],
854
871
  [:set_device_rgb_non_stroking_color, [1.0, 0.0, 0.0]],
855
872
  [:begin_text],
856
- [:set_text_matrix, [1, 0, 0, 1, 2, 23.609]],
873
+ [:move_text, [2, 23.609]],
857
874
  [:show_text, ["a"]],
858
875
  [:move_text_next_line],
859
876
  [:show_text, ["b"]],
@@ -159,11 +159,16 @@ describe HexaPDF::Type::AcroForm::Form do
159
159
  end
160
160
 
161
161
  def applies_variable_text_properties(method, **args)
162
- field = @acro_form.send(method, "field", **args, font: 'Times')
162
+ field = @acro_form.send(method, "field", **args)
163
163
  font_name, font_size, font_color = field.parse_default_appearance_string
164
- assert_equal(:'Times-Roman', @acro_form.default_resources.font(font_name)[:BaseFont])
164
+ assert_equal(:'Helvetica', @acro_form.default_resources.font(font_name)[:BaseFont])
165
165
  assert_equal(0, font_size)
166
166
  assert_equal(HexaPDF::Content::ColorSpace::DeviceGray.new.color(0), font_color)
167
+ assert_equal(0, field.value[:Q])
168
+
169
+ field = @acro_form.send(method, "field", **args, font: 'Times')
170
+ font_name, font_size, font_color = field.parse_default_appearance_string
171
+ assert_equal(:'Times-Roman', @acro_form.default_resources.font(font_name)[:BaseFont])
167
172
 
168
173
  field = @acro_form.send(method, "field", **args, font_options: {variant: :bold})
169
174
  font_name, = field.parse_default_appearance_string
@@ -171,7 +176,6 @@ describe HexaPDF::Type::AcroForm::Form do
171
176
 
172
177
  field = @acro_form.send(method, "field", **args, font_size: 10)
173
178
  font_name, font_size = field.parse_default_appearance_string
174
- assert_equal(:Helvetica, @acro_form.default_resources.font(font_name)[:BaseFont])
175
179
  assert_equal(10, font_size)
176
180
 
177
181
  field = @acro_form.send(method, "field", **args, font_color: "red")
@@ -507,11 +511,6 @@ describe HexaPDF::Type::AcroForm::Form do
507
511
  assert(@acro_form.validate)
508
512
  end
509
513
 
510
- it "set the default appearance string, though optional, to a valid value to avoid problems" do
511
- assert(@acro_form.validate)
512
- assert_equal("0.0 g /F1 0 Tf", @acro_form[:DA])
513
- end
514
-
515
514
  describe "field hierarchy validation" do
516
515
  before do
517
516
  @acro_form[:Fields] = [
@@ -31,7 +31,9 @@ describe HexaPDF::Type::AcroForm::SignatureField do
31
31
 
32
32
  it "gets the field value" do
33
33
  @field[:V] = {Empty: :True}
34
- assert_equal({Empty: :True}, @field.field_value.value)
34
+ value = @field.field_value
35
+ assert_kind_of(HexaPDF::DigitalSignature::Signature, value)
36
+ assert_equal({Empty: :True}, value)
35
37
  end
36
38
 
37
39
  it "validates the value of the /FT field" do
@@ -102,9 +102,22 @@ describe HexaPDF::Type::AcroForm::VariableTextField do
102
102
  @field.parse_default_appearance_string)
103
103
  end
104
104
 
105
- it "fails if no /DA value is set" do
105
+ it "sets a standard /DA value if no other /DA is found" do
106
106
  @doc.acro_form.delete(:DA)
107
+ assert_equal([:F1, 0, HexaPDF::Content::ColorSpace.prenormalized_device_color([0])],
108
+ @field.parse_default_appearance_string)
109
+ end
110
+
111
+ it "converts the /DA to a string in case an invalid PDF uses a Symbol" do
112
+ @field[:DA] = :"1 g /F1 20 Tf"
113
+ assert_equal([:F1, 20, @color], @field.parse_default_appearance_string)
114
+ end
115
+
116
+ it "fails if no /DA value is set and no default appearance string should be set" do
117
+ @doc.acro_form.delete(:DA)
118
+ @doc.config['acro_form.fallback_default_appearance'] = nil
107
119
  assert_raises(HexaPDF::Error) { @field.parse_default_appearance_string }
108
120
  end
121
+
109
122
  end
110
123
  end
@@ -14,10 +14,14 @@ describe HexaPDF::Type::Actions::Launch do
14
14
  it "needs a launch target" do
15
15
  refute(@action.validate)
16
16
 
17
- @action.value = {F: {}}
17
+ @action.value = {Win: {F: "test.exe"}}
18
+ assert(@action.validate)
19
+ @action.value = {Mac: 'test'}
20
+ assert(@action.validate)
21
+ @action.value = {Unix: 'test'}
18
22
  assert(@action.validate)
19
23
 
20
- @action.value = {Win: {F: "test.exe"}}
24
+ @action.value = {F: {}}
21
25
  assert(@action.validate)
22
26
  end
23
27
  end
@@ -194,6 +194,8 @@ describe HexaPDF::Type::Annotations::Widget do
194
194
 
195
195
  it "returns the default size if none is set" do
196
196
  assert_equal(0, @widget.marker_style.size)
197
+ @widget.form_field[:DA] = "0.0 g"
198
+ assert_equal(0, @widget.marker_style.size)
197
199
  end
198
200
 
199
201
  it "sets the given size" do
@@ -212,6 +214,8 @@ describe HexaPDF::Type::Annotations::Widget do
212
214
 
213
215
  it "returns the default color if none is set" do
214
216
  assert_equal([0], @widget.marker_style.color.components)
217
+ @widget.form_field[:DA] = "/ZaDb 10 Tfg"
218
+ assert_equal([0], @widget.marker_style.color.components)
215
219
  end
216
220
 
217
221
  it "sets the given color" do
@@ -138,5 +138,10 @@ describe HexaPDF::Type::FontType1 do
138
138
  @embedded_font.delete(:FontDescriptor)
139
139
  refute(@embedded_font.validate)
140
140
  end
141
+
142
+ it "ensures a correct Symbol value for the /Encoding key" do
143
+ @font[:Encoding] = :Other
144
+ refute(@font.validate)
145
+ end
141
146
  end
142
147
  end
@@ -57,7 +57,7 @@ describe HexaPDF::Type::Form do
57
57
  it "creates the resource dictionary if it is not found" do
58
58
  resources = @form.resources
59
59
  assert_equal(:XXResources, resources.type)
60
- assert_equal({ProcSet: [:PDF, :Text, :ImageB, :ImageC, :ImageI]}, resources.value)
60
+ assert_equal({}, resources.value)
61
61
  end
62
62
 
63
63
  it "returns the already used resource dictionary" do
@@ -358,6 +358,12 @@ describe HexaPDF::Type::Page do
358
358
  page[:Contents] = [@doc.wrap({}, stream: 'q 10'), @doc.wrap({}, stream: 'w Q')]
359
359
  assert_equal('q 10 w Q', page.contents)
360
360
  end
361
+
362
+ it "handles null objects in the /Contents array" do
363
+ page = @doc.pages.add
364
+ page[:Contents] = [@doc.wrap({}, stream: 'q 10'), nil]
365
+ assert_equal('q 10 ', page.contents)
366
+ end
361
367
  end
362
368
 
363
369
  describe "contents=" do
@@ -389,7 +395,7 @@ describe HexaPDF::Type::Page do
389
395
  page = @doc.add({Type: :Page, Parent: @doc.pages.root})
390
396
  resources = page.resources
391
397
  assert_equal(:XXResources, resources.type)
392
- assert_equal({ProcSet: [:PDF, :Text, :ImageB, :ImageC, :ImageI]}, resources.value)
398
+ assert_equal({}, resources.value)
393
399
  end
394
400
 
395
401
  it "returns the already used resource dictionary" 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.46.0
4
+ version: 1.0.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-08-11 00:00:00.000000000 Z
11
+ date: 2024-10-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: cmdparse
@@ -469,6 +469,7 @@ files:
469
469
  - lib/hexapdf/stream.rb
470
470
  - lib/hexapdf/task.rb
471
471
  - lib/hexapdf/task/dereference.rb
472
+ - lib/hexapdf/task/merge_acro_form.rb
472
473
  - lib/hexapdf/task/optimize.rb
473
474
  - lib/hexapdf/task/pdfa.rb
474
475
  - lib/hexapdf/test_utils.rb
@@ -594,21 +595,25 @@ files:
594
595
  - test/data/minimal.pdf
595
596
  - test/data/standard-security-handler/README
596
597
  - test/data/standard-security-handler/bothpwd-aes-128bit-V4.pdf
598
+ - test/data/standard-security-handler/bothpwd-aes-256bit-V5-R5.pdf
597
599
  - test/data/standard-security-handler/bothpwd-aes-256bit-V5.pdf
598
600
  - test/data/standard-security-handler/bothpwd-arc4-128bit-V2.pdf
599
601
  - test/data/standard-security-handler/bothpwd-arc4-128bit-V4.pdf
600
602
  - test/data/standard-security-handler/bothpwd-arc4-40bit-V1.pdf
601
603
  - test/data/standard-security-handler/nopwd-aes-128bit-V4.pdf
604
+ - test/data/standard-security-handler/nopwd-aes-256bit-V5-R5.pdf
602
605
  - test/data/standard-security-handler/nopwd-aes-256bit-V5.pdf
603
606
  - test/data/standard-security-handler/nopwd-arc4-128bit-V2.pdf
604
607
  - test/data/standard-security-handler/nopwd-arc4-128bit-V4.pdf
605
608
  - test/data/standard-security-handler/nopwd-arc4-40bit-V1.pdf
606
609
  - test/data/standard-security-handler/ownerpwd-aes-128bit-V4.pdf
610
+ - test/data/standard-security-handler/ownerpwd-aes-256bit-V5-R5.pdf
607
611
  - test/data/standard-security-handler/ownerpwd-aes-256bit-V5.pdf
608
612
  - test/data/standard-security-handler/ownerpwd-arc4-128bit-V2.pdf
609
613
  - test/data/standard-security-handler/ownerpwd-arc4-128bit-V4.pdf
610
614
  - test/data/standard-security-handler/ownerpwd-arc4-40bit-V1.pdf
611
615
  - test/data/standard-security-handler/userpwd-aes-128bit-V4.pdf
616
+ - test/data/standard-security-handler/userpwd-aes-256bit-V5-R5.pdf
612
617
  - test/data/standard-security-handler/userpwd-aes-256bit-V5.pdf
613
618
  - test/data/standard-security-handler/userpwd-arc4-128bit-V2.pdf
614
619
  - test/data/standard-security-handler/userpwd-arc4-128bit-V4.pdf
@@ -724,6 +729,7 @@ files:
724
729
  - test/hexapdf/layout/test_text_shaper.rb
725
730
  - test/hexapdf/layout/test_width_from_polygon.rb
726
731
  - test/hexapdf/task/test_dereference.rb
732
+ - test/hexapdf/task/test_merge_acro_form.rb
727
733
  - test/hexapdf/task/test_optimize.rb
728
734
  - test/hexapdf/task/test_pdfa.rb
729
735
  - test/hexapdf/test_composer.rb