hexapdf 1.9.1 → 1.11.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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +55 -0
  3. data/lib/hexapdf/cli/command.rb +1 -2
  4. data/lib/hexapdf/configuration.rb +2 -2
  5. data/lib/hexapdf/content/canvas.rb +2 -2
  6. data/lib/hexapdf/content/smart_text_extractor.rb +3 -2
  7. data/lib/hexapdf/dictionary_fields.rb +7 -0
  8. data/lib/hexapdf/digital_signature/cms_handler.rb +27 -2
  9. data/lib/hexapdf/digital_signature/pkcs1_handler.rb +5 -0
  10. data/lib/hexapdf/digital_signature/signatures.rb +105 -0
  11. data/lib/hexapdf/digital_signature/signing/signed_data_creator.rb +0 -2
  12. data/lib/hexapdf/document.rb +2 -2
  13. data/lib/hexapdf/encryption/security_handler.rb +3 -1
  14. data/lib/hexapdf/layout/style.rb +2 -2
  15. data/lib/hexapdf/layout/text_fragment.rb +5 -1
  16. data/lib/hexapdf/layout/text_shaper.rb +1 -57
  17. data/lib/hexapdf/parser.rb +1 -2
  18. data/lib/hexapdf/serializer.rb +1 -1
  19. data/lib/hexapdf/task/import_pages.rb +41 -9
  20. data/lib/hexapdf/type/acro_form/appearance_generator.rb +1 -1
  21. data/lib/hexapdf/type/acro_form/choice_field.rb +3 -1
  22. data/lib/hexapdf/type/acro_form/field.rb +1 -1
  23. data/lib/hexapdf/type/catalog.rb +8 -1
  24. data/lib/hexapdf/type/document_security_store.rb +81 -13
  25. data/lib/hexapdf/type/font_type0.rb +1 -1
  26. data/lib/hexapdf/version.rb +1 -1
  27. data/test/hexapdf/content/test_canvas.rb +3 -3
  28. data/test/hexapdf/content/test_smart_text_extractor.rb +7 -0
  29. data/test/hexapdf/digital_signature/common.rb +209 -2
  30. data/test/hexapdf/digital_signature/test_cms_handler.rb +4 -2
  31. data/test/hexapdf/digital_signature/test_pkcs1_handler.rb +4 -0
  32. data/test/hexapdf/digital_signature/test_signatures.rb +131 -0
  33. data/test/hexapdf/encryption/test_security_handler.rb +12 -0
  34. data/test/hexapdf/layout/test_style.rb +3 -1
  35. data/test/hexapdf/layout/test_text_fragment.rb +11 -4
  36. data/test/hexapdf/task/test_import_pages.rb +42 -1
  37. data/test/hexapdf/test_dictionary_fields.rb +8 -0
  38. data/test/hexapdf/test_parser.rb +25 -9
  39. data/test/hexapdf/test_serializer.rb +7 -1
  40. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +1 -1
  41. data/test/hexapdf/type/acro_form/test_choice_field.rb +5 -0
  42. data/test/hexapdf/type/acro_form/test_field.rb +3 -1
  43. data/test/hexapdf/type/test_catalog.rb +10 -0
  44. data/test/hexapdf/type/test_document_security_store.rb +120 -0
  45. data/test/test_helper.rb +7 -3
  46. metadata +939 -904
@@ -43,10 +43,18 @@ describe HexaPDF::Layout::TextFragment do
43
43
 
44
44
  it "allows using a style object instead of directly specifying style properties" do
45
45
  style = HexaPDF::Layout::Style.new(font: @font, font_size: 20)
46
- frags = HexaPDF::Layout::TextFragment.create_with_fallback_glyphs("Tom", style)
46
+ frags = HexaPDF::Layout::TextFragment.create_with_fallback_glyphs("Tom", style) {}
47
47
  assert_equal(37.78, frags[0].width)
48
48
  end
49
49
 
50
+ it "handles control characters separately by not running them through the text shaper" do
51
+ frags = HexaPDF::Layout::TextFragment.create_with_fallback_glyphs("\tA\nB\rC\r\nD", font: @font) {}
52
+ assert_equal(5, frags.size)
53
+ assert_equal("\t", frags[0].text)
54
+ assert_equal("A\n", frags[1].text)
55
+ assert_equal("C\r\n", frags[3].text)
56
+ end
57
+
50
58
  it "replaces invalid glyphs with the result of the block" do
51
59
  zapf_dingbats = @doc.fonts.add('ZapfDingbats')
52
60
  i = 0
@@ -62,7 +70,7 @@ describe HexaPDF::Layout::TextFragment do
62
70
 
63
71
  frags = HexaPDF::Layout::TextFragment.create_with_fallback_glyphs("✂Tom✂Tom✂Tom✂Tom\u{ad}",
64
72
  style, &fallback)
65
- assert_equal(8, frags.size)
73
+ assert_equal(7, frags.size)
66
74
  assert_equal(zapf_dingbats, frags[0].style.font)
67
75
  assert_equal(:a2, frags[0].items[0].name)
68
76
  assert_equal("Tom", frags[1].text)
@@ -71,8 +79,7 @@ describe HexaPDF::Layout::TextFragment do
71
79
  assert_equal("Tom", frags[3].text)
72
80
  assert_equal(:'.notdef', frags[4].items[0].name)
73
81
  assert_equal("Tom", frags[5].text)
74
- assert_equal("Tom", frags[6].text)
75
- assert_equal("\u{ad}", frags[7].text)
82
+ assert_equal("Tom\u{ad}", frags[6].text)
76
83
  end
77
84
  end
78
85
 
@@ -99,9 +99,10 @@ describe HexaPDF::Task::ImportPages do
99
99
  canvas = @doc.pages[0].canvas
100
100
  form = canvas.form
101
101
  form[:OC] = @ocg1
102
+ form.canvas.optional_content(@ocmd)
102
103
  canvas.xobject(form, at: [0, 0])
103
104
  @target.task(:import_pages, source: @doc)
104
- assert_equal(['OCG'], @target.optional_content.ocgs.map(&:name))
105
+ assert_equal(['OCG', 'OCMD'], @target.optional_content.ocgs.map(&:name))
105
106
  end
106
107
 
107
108
  it "preserves OCGs/OCMDs associated with annotations" do
@@ -123,4 +124,44 @@ describe HexaPDF::Task::ImportPages do
123
124
  assert_equal(['OCG'], rb_groups[0].map(&:name))
124
125
  end
125
126
  end
127
+
128
+ describe "resize_to argument" do
129
+ before do
130
+ @doc.annotations.create_line(@pages[0], start_point: [10, 10], end_point: [100, 100]).
131
+ regenerate_appearance
132
+ end
133
+
134
+ it "works when given a symbolic page size" do
135
+ @target.task(:import_pages, source: @doc, resize_to: :A4)
136
+ assert_equal(2, @doc.each.select {|obj| obj.type == :Page }.size)
137
+ assert_operators(@target.pages[0].contents, [[:paint_xobject, [:XO1]]])
138
+ assert_operators(@target.pages[1].contents, [[:paint_xobject, [:XO1]]])
139
+ end
140
+
141
+ it "works when given a media box directly" do
142
+ @target.task(:import_pages, source: @doc, pages: 0, resize_to: [5, 5, 100, 100])
143
+ assert_equal(1, @target.each.select {|obj| obj.type == :Page }.size)
144
+ assert_operators(@target.pages[0].contents,
145
+ [[:concatenate_matrix, [1, 0, 0, 1, 5, 5]],
146
+ [:save_graphics_state],
147
+ [:concatenate_matrix, [0.15959, 0, 0, 0.112841, 0.0, 0.0]],
148
+ [:paint_xobject, [:XO1]],
149
+ [:restore_graphics_state]])
150
+ end
151
+
152
+ it "flattens the annotations" do
153
+ @target.task(:import_pages, source: @doc, pages: 0, resize_to: [5, 5, 100, 100])
154
+ assert_operators(@target.pages[0].resources.xobject(:XO1).contents,
155
+ [[:save_graphics_state],
156
+ [:restore_graphics_state],
157
+ [:save_graphics_state],
158
+ [:restore_graphics_state],
159
+ [:save_graphics_state],
160
+ [:save_graphics_state],
161
+ [:concatenate_matrix, [1.0, 0, 0, 1.0, 0.0, 0.0]],
162
+ [:paint_xobject, [:XO1]],
163
+ [:restore_graphics_state],
164
+ [:restore_graphics_state]])
165
+ end
166
+ end
126
167
  end
@@ -129,6 +129,11 @@ describe HexaPDF::DictionaryFields do
129
129
  str = @field.convert("\xfe\xff\x00t\x00e\x00s\x00t".b, self)
130
130
  assert_equal('test', str)
131
131
  assert_equal(Encoding::UTF_8, str.encoding)
132
+
133
+ str = @field.convert("\xef\xbb\xbfHall\xC3\xB6".b, self)
134
+ assert_equal('Hallö', str)
135
+ assert_equal(Encoding::UTF_8, str.encoding)
136
+
132
137
  str = @field.convert("Testing\x9c\x92".b, self)
133
138
  assert_equal("Testing\u0153\u2122", str)
134
139
  assert_equal(Encoding::UTF_8, str.encoding)
@@ -141,6 +146,9 @@ describe HexaPDF::DictionaryFields do
141
146
  it "calls document.on_invalid_string if the provided string is invalid" do
142
147
  str = "\xfe\xff\xD8\x00\x00s\x00t".b
143
148
  assert_equal("st", @field.convert(str, self))
149
+
150
+ str = "\xef\xbb\xbfHall\xFF\xC3".b
151
+ assert_equal("Hall", @field.convert(str, self))
144
152
  end
145
153
  end
146
154
 
@@ -702,15 +702,6 @@ describe HexaPDF::Parser do
702
702
  assert_equal(6, @parser.load_object(@xref).value)
703
703
  end
704
704
 
705
- it "uses a security handler for decrypting indirect objects if necessary" do
706
- handler = Minitest::Mock.new
707
- handler.expect(:decrypt, HexaPDF::Object.new(:result, oid: 1), [HexaPDF::Object])
708
- @document.instance_variable_set(:@security_handler, handler)
709
- create_parser("1 0 obj\n6\nendobj\ntrailer\n<</Size 1>>")
710
- assert_equal(:result, @parser.load_object(@xref).value)
711
- assert(handler.verify)
712
- end
713
-
714
705
  it "ignores parts where the starting line is split across lines" do
715
706
  create_parser("1 0 obj\n5\nendobj\n1 0\nobj\n6\nendobj\ntrailer\n<</Size 1>>")
716
707
  assert_equal(5, @parser.load_object(@xref).value)
@@ -774,6 +765,31 @@ describe HexaPDF::Parser do
774
765
  assert_equal({Root: HexaPDF::Reference.new(1, 0)}, @parser.reconstructed_revision.trailer.value)
775
766
  end
776
767
 
768
+ it "decrypts strings and streams correctly in case of whole/part document recovery" do
769
+ io = StringIO.new
770
+ doc = HexaPDF::Document.new
771
+ doc.catalog[:XXTest] = doc.add({Data: "string"}, stream: "stream")
772
+ doc.encrypt
773
+ xref_section = doc.write(io)[1]
774
+ test_oid = doc.catalog[:XXTest].oid
775
+
776
+ # whole document corruption
777
+ invalid_doc = io.string.sub(/xref\n0 #{xref_section.max_oid + 1}/, "xref\n0 1")
778
+ doc = HexaPDF::Document.new(io: StringIO.new(invalid_doc))
779
+ assert(doc.revisions.parser.reconstructed?)
780
+ assert_equal('string', doc.catalog[:XXTest][:Data])
781
+ assert_equal('stream', doc.catalog[:XXTest].stream)
782
+
783
+ # part document corruption
784
+ invalid_doc = io.string.sub(xref_section[test_oid].pos.to_s.rjust(10, '0'),
785
+ (xref_section[test_oid].pos + 5).to_s.rjust(10, '0'))
786
+ doc = HexaPDF::Document.new(io: StringIO.new(invalid_doc))
787
+ refute(doc.revisions.parser.reconstructed?)
788
+ assert_equal('string', doc.catalog[:XXTest][:Data])
789
+ assert_equal('stream', doc.catalog[:XXTest].stream)
790
+ assert(doc.revisions.parser.reconstructed?)
791
+ end
792
+
777
793
  it "fails if no valid trailer is found and couldn't be constructed" do
778
794
  create_parser("1 0 obj\n5\nendobj\nquack trailer <</Size 1>>\nstartxref\n22\n%%EOF")
779
795
  assert_raises(HexaPDF::MalformedPDFError) { @parser.reconstructed_revision.trailer }
@@ -58,7 +58,7 @@ describe HexaPDF::Serializer do
58
58
  assert_serialized("1208925819614629174706176", 1_208_925_819_614_629_174_706_176)
59
59
  end
60
60
 
61
- it "serializes floats with a precision of 4" do
61
+ it "serializes floats with a precision of 6" do
62
62
  assert_serialized("1.5", 1.5)
63
63
  assert_serialized("-1.5", -1.5)
64
64
  assert_serialized("9.123456", 9.123456)
@@ -66,6 +66,12 @@ describe HexaPDF::Serializer do
66
66
  assert_serialized("0.000005", 0.000005)
67
67
  assert_serialized("-0.000005", -0.000005)
68
68
  assert_serialized("0.0", 0.0)
69
+ assert_serialized("123456789012345.0", 1.23456789012345e14)
70
+ assert_serialized("123456789012345.34", 1.2345678901234534e14)
71
+ assert_serialized("-123456789012345.0", -1.23456789012345e14)
72
+ assert_serialized("-123456789012345.34", -1.2345678901234534e14)
73
+ assert_serialized("999999999999999", 1.23456789012345e15)
74
+ assert_serialized("-999999999999999", -1.23456789012345e15)
69
75
  assert_raises(HexaPDF::Error) { @serializer.serialize(0.0 / 0) }
70
76
  assert_raises(HexaPDF::Error) { @serializer.serialize(1.0 / 0) }
71
77
  assert_raises(HexaPDF::Error) { @serializer.serialize(-1.0 / 0) }
@@ -928,7 +928,7 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
928
928
 
929
929
  it "creates the /N appearance stream" do
930
930
  @field[:I] = [1, 2]
931
- @field[:V] = ['b', 'c']
931
+ @field[:V] = ['b', 'c', 'd']
932
932
  @field.set_default_appearance_string(font_size: 12, font_color: "red")
933
933
  @generator.create_appearances
934
934
  assert_operators(@widget[:AP][:N].stream,
@@ -103,6 +103,11 @@ describe HexaPDF::Type::AcroForm::ChoiceField do
103
103
  @field.option_items = [1, 2, 3, 4]
104
104
  @field.list_box_top_index = 2
105
105
  assert_equal(2, @field.list_box_top_index)
106
+
107
+ @field[:TI] = 100
108
+ assert_equal(0, @field.list_box_top_index)
109
+ @field[:TI] = "other"
110
+ assert_equal(0, @field.list_box_top_index)
106
111
  end
107
112
 
108
113
  it "fails if mulitple values are provided but the list box is not a multi-select" do
@@ -113,7 +113,9 @@ describe HexaPDF::Type::AcroForm::Field do
113
113
 
114
114
  it "can check whether a widget is embedded in the field" do
115
115
  refute(@field.embedded_widget?)
116
- @field[:Subtype] = :Wdiget
116
+ @field[:Subtype] = :Link
117
+ refute(@field.embedded_widget?)
118
+ @field[:Subtype] = :Widget
117
119
  assert(@field.embedded_widget?)
118
120
  end
119
121
 
@@ -50,6 +50,16 @@ describe HexaPDF::Type::Catalog do
50
50
  assert_equal(:XXOCConfiguration, oc[:D].type)
51
51
  end
52
52
 
53
+ it "uses or creates the document security store on access" do
54
+ @catalog[:DSS] = {}
55
+ assert_equal(:DSS, @catalog.dss.type)
56
+
57
+ @catalog.delete(:DSS)
58
+ dss = @catalog.dss
59
+ assert_equal(:DSS, dss.type)
60
+ assert_same(dss, @catalog.dss)
61
+ end
62
+
53
63
  describe "acro_form" do
54
64
  it "returns an existing form object" do
55
65
  @catalog[:AcroForm] = :test
@@ -0,0 +1,120 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'test_helper'
4
+ require_relative '../digital_signature/common'
5
+ require 'hexapdf/document'
6
+ require 'hexapdf/type/document_security_store'
7
+
8
+ describe HexaPDF::Type::DocumentSecurityStore do
9
+ before do
10
+ @doc = HexaPDF::Document.new
11
+ @dss = @doc.add({Type: :DSS})
12
+ end
13
+
14
+ [[:add_cert, :Certs], [:add_ocsp, :OCSPs], [:add_crl, :CRLs]].each do |method, field|
15
+ describe method do
16
+ it "adds a #{field[0..-2]} as an indirect stream object" do
17
+ result = @dss.send(method, "der_data")
18
+ assert_kind_of(HexaPDF::Stream, result)
19
+ assert_equal(:FlateDecode, result[:Filter])
20
+ assert_equal("der_data", result.stream)
21
+ assert_equal(1, @dss[field].size)
22
+ end
23
+
24
+ it "returns the same stream object for duplicate #{field[0..-2]} data" do
25
+ result1 = @dss.send(method, "der_data")
26
+ result2 = @dss.send(method, "der_data")
27
+ assert_same(result1, result2)
28
+ assert_equal(1, @dss[field].size)
29
+ end
30
+
31
+ it "adds distinct stream objects for different #{field[0..-2]} data" do
32
+ @dss.send(method, "data_one")
33
+ @dss.send(method, "data_two")
34
+ assert_equal(2, @dss[field].size)
35
+ end
36
+ end
37
+ end
38
+
39
+ describe "add_vri" do
40
+ before do
41
+ @signature_contents = 'signature bytes'
42
+ @signature = @doc.add({Type: :Sig, Contents: @signature_contents})
43
+ @vri_key = OpenSSL::Digest::SHA1.hexdigest(@signature_contents).upcase.to_sym
44
+ end
45
+
46
+ it "returns the VRI entry" do
47
+ vri = @dss.add_vri(@signature)
48
+ assert_equal(:VRI, vri.type)
49
+ end
50
+
51
+ it "creates the /VRI dictionary with an entry for the signature" do
52
+ vri = @dss.add_vri(@signature)
53
+ assert(@dss.key?(:VRI))
54
+ assert_same(vri, @dss[:VRI][@vri_key])
55
+ end
56
+
57
+ it "populates /Cert in the VRI entry and the DSS /Certs array" do
58
+ @dss.add_vri(@signature, certs: ["cert1", "cert2"])
59
+ assert_equal(2, @dss[:VRI][@vri_key][:Cert].size)
60
+ assert_equal(2, @dss[:Certs].size)
61
+ end
62
+
63
+ it "populates /OCSP in the VRI entry and the DSS /OCSPs array" do
64
+ @dss.add_vri(@signature, ocsps: ["ocsp1", "ocsp2"])
65
+ assert_equal(2, @dss[:VRI][@vri_key][:OCSP].size)
66
+ assert_equal(2, @dss[:OCSPs].size)
67
+ end
68
+
69
+ it "populates /CRL in the VRI entry and the DSS /CRLs array" do
70
+ @dss.add_vri(@signature, crls: ["crl1"])
71
+ assert_equal(1, @dss[:VRI][@vri_key][:CRL].size)
72
+ assert_equal(1, @dss[:CRLs].size)
73
+ end
74
+
75
+ it "de-duplicates DER streams shared across multiple VRI entries" do
76
+ @dss.add_vri(@signature, certs: ["shared_cert", "cert_a"])
77
+ @dss.add_vri(@doc.add({Type: :Sig, Contents: 'new'}), certs: ["shared_cert", "cert_b"])
78
+ # shared_cert added once despite appearing in both VRI entries
79
+ assert_equal(2, @dss[:VRI].value.size)
80
+ assert_equal(3, @dss[:Certs].size)
81
+ end
82
+
83
+ it "creates a VRI entry with no optional arrays when all inputs are empty" do
84
+ @dss.add_vri(@signature)
85
+ vri = @dss[:VRI][@vri_key]
86
+ refute(vri.key?(:Cert))
87
+ refute(vri.key?(:OCSP))
88
+ refute(vri.key?(:CRL))
89
+ end
90
+ end
91
+
92
+ describe "vri_for" do
93
+ before do
94
+ @signature = @doc.add({Type: :Sig, Contents: 'signature bytes'})
95
+ end
96
+
97
+ it "returns nil if no /VRI entry exists" do
98
+ assert_nil(@dss.vri_for(@signature))
99
+ end
100
+
101
+ it "returns the VRI entry for the signature if it exists" do
102
+ @dss[:VRI] = {}
103
+ assert_nil(@dss.vri_for(@signature))
104
+ @dss.add_vri(@signature)
105
+ assert_equal(@dss[:VRI].value.first[1], @dss.vri_for(@signature))
106
+ end
107
+ end
108
+
109
+ describe "certificates" do
110
+ it "returns an empty array if no /Certs entry exists" do
111
+ assert_equal([], @dss.certificates)
112
+ end
113
+
114
+ it "returns an array with certificate instances" do
115
+ cert = CERTIFICATES.signer_certificate
116
+ @dss[:Certs] = [@doc.add({}, stream: cert.to_der)]
117
+ assert_equal([cert], @dss.certificates)
118
+ end
119
+ end
120
+ end
data/test/test_helper.rb CHANGED
@@ -3,9 +3,13 @@
3
3
  begin
4
4
  require 'simplecov'
5
5
  SimpleCov.start do
6
- minimum_coverage line: 100 unless ENV['NO_SIMPLECOV']
7
- add_filter '/test/'
8
- add_filter '/fast_arc4.rb'
6
+ enable_coverage(:method)
7
+ unless ENV['NO_SIMPLECOV']
8
+ coverage(:line) { minimum 100 }
9
+ coverage(:method) { minimum 100 }
10
+ end
11
+ skip '/test/'
12
+ skip '/fast_arc4.rb'
9
13
  end
10
14
  rescue LoadError
11
15
  end