hexapdf 1.9.1 → 1.10.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +25 -0
- data/lib/hexapdf/configuration.rb +2 -2
- data/lib/hexapdf/content/smart_text_extractor.rb +3 -2
- data/lib/hexapdf/dictionary_fields.rb +7 -0
- data/lib/hexapdf/digital_signature/cms_handler.rb +27 -2
- data/lib/hexapdf/digital_signature/pkcs1_handler.rb +5 -0
- data/lib/hexapdf/encryption/security_handler.rb +3 -1
- data/lib/hexapdf/layout/text_fragment.rb +5 -1
- data/lib/hexapdf/parser.rb +1 -2
- data/lib/hexapdf/serializer.rb +1 -1
- data/lib/hexapdf/type/catalog.rb +8 -1
- data/lib/hexapdf/type/document_security_store.rb +67 -13
- data/lib/hexapdf/type/font_type0.rb +1 -1
- data/lib/hexapdf/version.rb +1 -1
- data/test/hexapdf/content/test_smart_text_extractor.rb +7 -0
- data/test/hexapdf/digital_signature/test_cms_handler.rb +4 -2
- data/test/hexapdf/digital_signature/test_pkcs1_handler.rb +4 -0
- data/test/hexapdf/encryption/test_security_handler.rb +12 -0
- data/test/hexapdf/layout/test_text_fragment.rb +11 -4
- data/test/hexapdf/test_dictionary_fields.rb +8 -0
- data/test/hexapdf/test_parser.rb +25 -9
- data/test/hexapdf/test_serializer.rb +7 -1
- data/test/hexapdf/type/test_catalog.rb +10 -0
- data/test/hexapdf/type/test_document_security_store.rb +90 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0c85f671cd9d2e469b7f0e8c1ea2f9beec6a09247881c2404b8c9789ba65cd03
|
|
4
|
+
data.tar.gz: e6d53dd3da7a366f5b724448d31bc7edacae9acb65f6f4fe95e6e3c5caf27564
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2572d491909fa5f4574705de076a2889d7a0869f6cb3358787394b02ed00ec7e0763617515f9f2ecb50718f6b584189f88e0dad0ab612f0472b8551402331628
|
|
7
|
+
data.tar.gz: cfe5c5efeaa319b781c566a3d1b0034bf1876ddc8100fecdb96257f68064779476f579e266e796b5a5740177b899319c1fde9e4f21b355d14054e5f157cb3e30
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,28 @@
|
|
|
1
|
+
## 1.10.0 - 2026-08-21
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
|
|
5
|
+
* Support for PDF 2.0 UTF-8 strings to
|
|
6
|
+
[HexaPDF::DictionaryFields::StringConverter]
|
|
7
|
+
* [HexaPDF::Type::Catalog#dss] for returning the document security store
|
|
8
|
+
* [HexaPDF::DigitalSignature::PKCS1Handler#embedded_tsa_signature]
|
|
9
|
+
* [HexaPDF::Type::DocumentSecurityStore] convenience methods for adding entries
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
* Removed invalid dictionary fields from [HexaPDF::Type::DocumentSecurityStore]
|
|
14
|
+
* [HexaPDF::DigitalSignature::CMSHandler] to handle padding of /Contents
|
|
15
|
+
correctly
|
|
16
|
+
* Regression in [HexaPDF::Layout::TextFragment::create_with_fallback_glyphs]
|
|
17
|
+
with respect to handling `\r\n` as a single newline
|
|
18
|
+
* Double decryption when reconstructing invalid files
|
|
19
|
+
* Parsing encrypted indirect objects that should be streams but aren't
|
|
20
|
+
* Serializing of large floats that used scientific instead of fixed-point
|
|
21
|
+
notation
|
|
22
|
+
* [HexaPDF::Content::SmartTextExtractor::layout_text_runs] to work in case of
|
|
23
|
+
degenerate input where the majority of text runs have zero height
|
|
24
|
+
|
|
25
|
+
|
|
1
26
|
## 1.9.1 - 2026-06-09
|
|
2
27
|
|
|
3
28
|
### Fixed
|
|
@@ -254,8 +254,8 @@ module HexaPDF
|
|
|
254
254
|
# In nearly all cases this option should not be changed from its default setting!
|
|
255
255
|
#
|
|
256
256
|
# document.on_invalid_string::
|
|
257
|
-
# A callable object that takes the invalid UTF-16BE encoded string and returns a valid
|
|
258
|
-
# encoded string.
|
|
257
|
+
# A callable object that takes the invalid UTF-16BE or UTF-8 encoded string and returns a valid
|
|
258
|
+
# UTF-8 encoded string.
|
|
259
259
|
#
|
|
260
260
|
# The default is to remove all invalid characters.
|
|
261
261
|
#
|
|
@@ -172,8 +172,9 @@ module HexaPDF
|
|
|
172
172
|
return '' if text_runs.empty?
|
|
173
173
|
|
|
174
174
|
# Use the median height of all text runs as an approximation of the main font size used on
|
|
175
|
-
# the page.
|
|
176
|
-
|
|
175
|
+
# the page. In case the majority of text runs have a height of 0, use a non-zero height
|
|
176
|
+
# value. The line tolerance uses a hard floor for small fonts.
|
|
177
|
+
median_height = [median(text_runs.map(&:height).sort), 1].max
|
|
177
178
|
line_tolerance = [median_height * line_tolerance_factor, 2].max
|
|
178
179
|
|
|
179
180
|
# Group the text runs into lines which are sorted top to bottom. Text runs are pre-sorted by
|
|
@@ -262,6 +262,13 @@ module HexaPDF
|
|
|
262
262
|
else
|
|
263
263
|
document.config['document.on_invalid_string'].call(str)
|
|
264
264
|
end
|
|
265
|
+
elsif str.getbyte(0) == 239 && str.getbyte(1) == 187 && str.getbyte(2) == 191
|
|
266
|
+
str = str[3..-1].force_encoding(Encoding::UTF_8)
|
|
267
|
+
if str.valid_encoding?
|
|
268
|
+
str
|
|
269
|
+
else
|
|
270
|
+
document.config['document.on_invalid_string'].call(str)
|
|
271
|
+
end
|
|
265
272
|
else
|
|
266
273
|
Utils::PDFDocEncoding.convert_to_utf8(str)
|
|
267
274
|
end
|
|
@@ -92,7 +92,7 @@ module HexaPDF
|
|
|
92
92
|
return @embedded_tsa_signature if defined?(@embedded_tsa_signature)
|
|
93
93
|
|
|
94
94
|
@embedded_tsa_signature = nil
|
|
95
|
-
p7 =
|
|
95
|
+
p7 = decode_asn1(signature_dict.contents)
|
|
96
96
|
signed_data = p7.value[1].value[0]
|
|
97
97
|
signer_info = signed_data.value[-1].value[0] # first (and only) signer info
|
|
98
98
|
return unless signer_info.value[-1].tag == 1 # check for unsigned attributes
|
|
@@ -140,7 +140,7 @@ module HexaPDF
|
|
|
140
140
|
|
|
141
141
|
if signature_dict.signature_type == 'ETSI.RFC3161'
|
|
142
142
|
# Getting the needed values is not directly supported by Ruby OpenSSL
|
|
143
|
-
p7 =
|
|
143
|
+
p7 = decode_asn1(signature_dict.contents)
|
|
144
144
|
signed_data = p7.value[1].value[0]
|
|
145
145
|
content_info = signed_data.value[2]
|
|
146
146
|
content = OpenSSL::ASN1.decode(content_info.value[1].value[0].value)
|
|
@@ -175,6 +175,31 @@ module HexaPDF
|
|
|
175
175
|
result
|
|
176
176
|
end
|
|
177
177
|
|
|
178
|
+
private
|
|
179
|
+
|
|
180
|
+
# Decode the first data structure in the given +data+ binary string.
|
|
181
|
+
#
|
|
182
|
+
# Since ASN1.decode raises an error if there are trailing bytes in +data+, we need to try
|
|
183
|
+
# several things to get the first data structure out of +data+ that is possibly zero-padded
|
|
184
|
+
# (due to definite and indefinite encodings; \x00\x00 is the EOD marker for indefinite
|
|
185
|
+
# encodings complicating things).
|
|
186
|
+
def decode_asn1(data)
|
|
187
|
+
length = OpenSSL::ASN1.traverse(data) do |_depth, _offset, header_length, length, *|
|
|
188
|
+
break length > 0 ? header_length + length : 0
|
|
189
|
+
end
|
|
190
|
+
if length > 0
|
|
191
|
+
OpenSSL::ASN1.decode(data[0, length])
|
|
192
|
+
else
|
|
193
|
+
begin
|
|
194
|
+
OpenSSL::ASN1.decode(data)
|
|
195
|
+
rescue OpenSSL::ASN1::ASN1Error => e
|
|
196
|
+
length = e.message.scan(/Total bytes read: (\d+)/)&.first&.first.to_i
|
|
197
|
+
data = data[0, length]
|
|
198
|
+
retry
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
178
203
|
end
|
|
179
204
|
|
|
180
205
|
end
|
|
@@ -59,6 +59,11 @@ module HexaPDF
|
|
|
59
59
|
certificate_chain.first
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
+
# Returns +nil+.
|
|
63
|
+
def embedded_tsa_signature
|
|
64
|
+
nil
|
|
65
|
+
end
|
|
66
|
+
|
|
62
67
|
# Verifies the signature using the provided OpenSSL::X509::Store object.
|
|
63
68
|
def verify(store, allow_self_signed: false)
|
|
64
69
|
result = super
|
|
@@ -276,7 +276,9 @@ module HexaPDF
|
|
|
276
276
|
str.replace(string_algorithm.decrypt(key, str, &error_proc))
|
|
277
277
|
end
|
|
278
278
|
|
|
279
|
-
|
|
279
|
+
# The (obj.raw_stream == '') case may occur for PDFs where a typed object that should be a
|
|
280
|
+
# stream isn't one. For example, if a /Type /Form object doesn't have stream ... endstream.
|
|
281
|
+
if obj.kind_of?(HexaPDF::Stream) && obj.raw_stream != '' && obj.raw_stream.filter[0] != :Crypt
|
|
280
282
|
unless string_algorithm == stream_algorithm
|
|
281
283
|
key = object_key(obj.oid, obj.gen, stream_algorithm)
|
|
282
284
|
end
|
|
@@ -119,7 +119,11 @@ module HexaPDF
|
|
|
119
119
|
items = []
|
|
120
120
|
end
|
|
121
121
|
if glyph.control_char?
|
|
122
|
-
result.
|
|
122
|
+
if result.last&.style == style
|
|
123
|
+
result.last.items << glyph
|
|
124
|
+
else
|
|
125
|
+
result.append(new([glyph], style))
|
|
126
|
+
end
|
|
123
127
|
else
|
|
124
128
|
fallback = yield(codepoint, glyph)
|
|
125
129
|
unless fallback.empty?
|
data/lib/hexapdf/parser.rb
CHANGED
|
@@ -531,8 +531,7 @@ module HexaPDF
|
|
|
531
531
|
|
|
532
532
|
loader = lambda do |xref_entry|
|
|
533
533
|
obj, oid, gen, stream = parse_indirect_object(xref_entry.pos)
|
|
534
|
-
|
|
535
|
-
@document.security_handler ? @document.security_handler.decrypt(obj) : obj
|
|
534
|
+
@document.wrap(obj, oid: oid, gen: gen, stream: stream)
|
|
536
535
|
end
|
|
537
536
|
|
|
538
537
|
@in_reconstruct_revision = false
|
data/lib/hexapdf/serializer.rb
CHANGED
|
@@ -206,7 +206,7 @@ module HexaPDF
|
|
|
206
206
|
if -0.0001 < obj && obj < 0.0001 && obj != 0
|
|
207
207
|
sprintf("%.6f", obj)
|
|
208
208
|
elsif obj.finite?
|
|
209
|
-
obj.round(6).to_s
|
|
209
|
+
obj.clamp(-999999999999999, 999999999999999).round(6).to_s
|
|
210
210
|
else
|
|
211
211
|
raise HexaPDF::Error, "Can't serialize special floating point number #{obj}"
|
|
212
212
|
end
|
data/lib/hexapdf/type/catalog.rb
CHANGED
|
@@ -84,7 +84,7 @@ module HexaPDF
|
|
|
84
84
|
define_field :Requirements, type: PDFArray, version: '1.7'
|
|
85
85
|
define_field :Collection, type: Dictionary, version: '1.7'
|
|
86
86
|
define_field :NeedsRendering, type: Boolean, version: '1.7'
|
|
87
|
-
define_field :DSS, type:
|
|
87
|
+
define_field :DSS, type: :DSS, version: '2.0'
|
|
88
88
|
define_field :AF, type: PDFArray, version: '2.0'
|
|
89
89
|
define_field :DPartRoot, type: Dictionary, version: '2.0'
|
|
90
90
|
|
|
@@ -124,6 +124,13 @@ module HexaPDF
|
|
|
124
124
|
self[:OCProperties] ||= document.add({OCGs: [], D: {Creator: 'HexaPDF'}}, type: :XXOCProperties)
|
|
125
125
|
end
|
|
126
126
|
|
|
127
|
+
# Returns the document security store, creating it if needed.
|
|
128
|
+
#
|
|
129
|
+
# See: DocumentSecurityStore
|
|
130
|
+
def dss
|
|
131
|
+
self[:DSS] ||= document.add({}, type: :DSS)
|
|
132
|
+
end
|
|
133
|
+
|
|
127
134
|
# Returns the main AcroForm object.
|
|
128
135
|
#
|
|
129
136
|
# * If an AcroForm object exists, the +create+ argument is not used.
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
# commercial licenses are available at <https://gettalong.at/hexapdf/>.
|
|
35
35
|
#++
|
|
36
36
|
|
|
37
|
+
require 'openssl'
|
|
37
38
|
require 'hexapdf/dictionary'
|
|
38
39
|
|
|
39
40
|
module HexaPDF
|
|
@@ -54,25 +55,78 @@ module HexaPDF
|
|
|
54
55
|
define_type :VRI
|
|
55
56
|
|
|
56
57
|
define_field :Type, type: Symbol, default: type
|
|
57
|
-
define_field :Cert, type: PDFArray
|
|
58
|
-
define_field :CRL, type: PDFArray
|
|
59
|
-
define_field :OCSP, type: PDFArray
|
|
60
|
-
define_field :TU, type: PDFDate
|
|
61
|
-
define_field :TS, type: Stream
|
|
58
|
+
define_field :Cert, type: PDFArray, version: '2.0'
|
|
59
|
+
define_field :CRL, type: PDFArray, version: '2.0'
|
|
60
|
+
define_field :OCSP, type: PDFArray, version: '2.0'
|
|
61
|
+
define_field :TU, type: PDFDate, version: '2.0'
|
|
62
|
+
define_field :TS, type: Stream, version: '2.0'
|
|
62
63
|
|
|
63
64
|
end
|
|
64
65
|
|
|
65
66
|
define_type :DSS
|
|
66
67
|
|
|
67
68
|
define_field :Type, type: Symbol, default: type
|
|
68
|
-
define_field :VRI, type: Dictionary
|
|
69
|
-
define_field :Certs, type: PDFArray
|
|
70
|
-
define_field :OCSPs, type: PDFArray
|
|
71
|
-
define_field :CRLs, type: PDFArray
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
69
|
+
define_field :VRI, type: Dictionary, version: '2.0'
|
|
70
|
+
define_field :Certs, type: PDFArray, version: '2.0'
|
|
71
|
+
define_field :OCSPs, type: PDFArray, version: '2.0'
|
|
72
|
+
define_field :CRLs, type: PDFArray, version: '2.0'
|
|
73
|
+
|
|
74
|
+
# Adds validation data for a single signature to the /VRI dictionary and to the various
|
|
75
|
+
# arrays, and returns the new VRI entry.
|
|
76
|
+
#
|
|
77
|
+
# +signature+::
|
|
78
|
+
# The signature for which the validation data should be added.
|
|
79
|
+
#
|
|
80
|
+
# +certs+::
|
|
81
|
+
# Array of DER-encoded certificates.
|
|
82
|
+
#
|
|
83
|
+
# +ocsps+::
|
|
84
|
+
# Array of DER-encoded OCSP responses.
|
|
85
|
+
#
|
|
86
|
+
# +crls+::
|
|
87
|
+
# Array of DER-encoded CRLs.
|
|
88
|
+
def add_vri(signature, certs: [], ocsps: [], crls: [])
|
|
89
|
+
key = OpenSSL::Digest::SHA1.hexdigest(signature.contents).upcase.to_sym
|
|
90
|
+
vri = {Type: :VRI}
|
|
91
|
+
vri[:Cert] = certs.map {|der| add_cert(der) } unless certs.empty?
|
|
92
|
+
vri[:OCSP] = ocsps.map {|der| add_ocsp(der) } unless ocsps.empty?
|
|
93
|
+
vri[:CRL] = crls.map {|der| add_crl(der) } unless crls.empty?
|
|
94
|
+
(self[:VRI] ||= {})[key] = document.add(vri)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Adds the DER-encoded certificate to the /Certs array if not already present and returns
|
|
98
|
+
# the stream object containing it.
|
|
99
|
+
def add_cert(cert_der)
|
|
100
|
+
add_data_as_stream_to_field(cert_der, :Certs)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Adds the DER-encoded OCSP response to the /OCSPs array if not already present and returns
|
|
104
|
+
# the stream object containing it.
|
|
105
|
+
def add_ocsp(ocsp_der)
|
|
106
|
+
add_data_as_stream_to_field(ocsp_der, :OCSPs)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Adds the DER-encoded CRL to the /CRLs array if not already present and returns the stream
|
|
110
|
+
# object containing it.
|
|
111
|
+
def add_crl(crl_der)
|
|
112
|
+
add_data_as_stream_to_field(crl_der, :CRLs)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
# Adds the given string +data+ to the +field+ array as stream object and returns the resulting
|
|
118
|
+
# stream.
|
|
119
|
+
#
|
|
120
|
+
# If the field already contains a stream with the same data, this existing stream is returned.
|
|
121
|
+
def add_data_as_stream_to_field(data, field)
|
|
122
|
+
self[field] ||= []
|
|
123
|
+
existing_stream = self[field].find {|stream| stream.stream == data }
|
|
124
|
+
return existing_stream if existing_stream
|
|
125
|
+
|
|
126
|
+
stream = document.add({Filter: :FlateDecode}, stream: data)
|
|
127
|
+
self[field] << stream
|
|
128
|
+
stream
|
|
129
|
+
end
|
|
76
130
|
|
|
77
131
|
end
|
|
78
132
|
|
|
@@ -146,7 +146,7 @@ module HexaPDF
|
|
|
146
146
|
registry = system_info[:Registry]
|
|
147
147
|
ordering = system_info[:Ordering]
|
|
148
148
|
if (encoding.kind_of?(Symbol) && HexaPDF::Font::CMap.predefined?(encoding.to_s) &&
|
|
149
|
-
|
|
149
|
+
encoding != :'Identity-H' && encoding != :'Identity-V') ||
|
|
150
150
|
(registry == "Adobe" && ['GB1', 'CNS1', 'Japan1', 'Korea1'].include?(ordering))
|
|
151
151
|
HexaPDF::Font::CMap.for_name("#{registry}-#{ordering}-UCS2")
|
|
152
152
|
end
|
data/lib/hexapdf/version.rb
CHANGED
|
@@ -101,6 +101,13 @@ describe HexaPDF::Content::SmartTextExtractor do
|
|
|
101
101
|
['Foot', 50, 10, 66, 20]]))
|
|
102
102
|
end
|
|
103
103
|
|
|
104
|
+
|
|
105
|
+
it "works in case the majority of text runs have zero height" do
|
|
106
|
+
assert_equal(["Hello", "World News"].join("\n"*17), layout_runs([['World', 50, 80, 70, 80],
|
|
107
|
+
['News', 75, 80, 100, 80],
|
|
108
|
+
['Hello', 50, 100, 70, 100]]))
|
|
109
|
+
end
|
|
110
|
+
|
|
104
111
|
it "ignores outliers when calculating the normal line spacing" do
|
|
105
112
|
assert_equal("Hello\nWorld\n\n\n\nHere",
|
|
106
113
|
layout_runs([['Hello', 50, 100, 70, 110],
|
|
@@ -125,7 +125,9 @@ describe HexaPDF::DigitalSignature::CMSHandler do
|
|
|
125
125
|
fac.serial_number = 1
|
|
126
126
|
fac.allowed_digests = ["sha256", "sha512"]
|
|
127
127
|
res = fac.create_timestamp(CERTIFICATES.signer_key, CERTIFICATES.timestamp_certificate, req)
|
|
128
|
-
|
|
128
|
+
der_form = res.token.to_der
|
|
129
|
+
# Convert DER to BER with indefinite length encoding as variation
|
|
130
|
+
@dict.contents = der_form[0] << "\x80".b << der_form[4..-1] << "\x00\x00".b << "\x00\x00\x00\x00".b
|
|
129
131
|
@dict.signature_type = 'ETSI.RFC3161'
|
|
130
132
|
@handler = HexaPDF::DigitalSignature::CMSHandler.new(@dict)
|
|
131
133
|
|
|
@@ -151,7 +153,7 @@ describe HexaPDF::DigitalSignature::CMSHandler do
|
|
|
151
153
|
key: CERTIFICATES.signer_key, timestamp_handler: tsh,
|
|
152
154
|
certificates: [CERTIFICATES.ca_certificate]
|
|
153
155
|
)
|
|
154
|
-
@dict.contents = cms.to_der
|
|
156
|
+
@dict.contents = cms.to_der << "\x00\x00\x00\x00"
|
|
155
157
|
@dict.signed_data = @data
|
|
156
158
|
@handler = HexaPDF::DigitalSignature::CMSHandler.new(@dict)
|
|
157
159
|
end
|
|
@@ -28,6 +28,10 @@ describe HexaPDF::DigitalSignature::PKCS1Handler do
|
|
|
28
28
|
assert_equal(CERTIFICATES.signer_certificate, @handler.signer_certificate)
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
it "returns nil for the embedded timestamp signature" do
|
|
32
|
+
assert_nil(@handler.embedded_tsa_signature)
|
|
33
|
+
end
|
|
34
|
+
|
|
31
35
|
describe "verify" do
|
|
32
36
|
before do
|
|
33
37
|
@store = OpenSSL::X509::Store.new
|
|
@@ -302,6 +302,18 @@ describe HexaPDF::Encryption::SecurityHandler do
|
|
|
302
302
|
assert_equal('string', obj.stream)
|
|
303
303
|
end
|
|
304
304
|
|
|
305
|
+
it "handles decryption of should-be stream objects without actual streams" do
|
|
306
|
+
doc = HexaPDF::Document.new
|
|
307
|
+
obj = doc.add({}) # Create XObject as Dictionary and not as Stream
|
|
308
|
+
obj[:Type] = :XObject
|
|
309
|
+
obj[:Subtype] = :Form
|
|
310
|
+
doc.catalog[:Test] = obj
|
|
311
|
+
doc.encrypt
|
|
312
|
+
doc = HexaPDF::Document.new(io: StringIO.new(doc.write_to_string))
|
|
313
|
+
assert_kind_of(HexaPDF::Stream, doc.catalog[:Test])
|
|
314
|
+
assert_equal('', doc.catalog[:Test].raw_stream)
|
|
315
|
+
end
|
|
316
|
+
|
|
305
317
|
it "doesn't decrypt a document's Encrypt dictionaries" do
|
|
306
318
|
@document = HexaPDF::Document.new
|
|
307
319
|
@document.trailer[:Encrypt] = @document.add({Key: "Something"})
|
|
@@ -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(
|
|
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
|
|
|
@@ -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
|
|
data/test/hexapdf/test_parser.rb
CHANGED
|
@@ -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
|
|
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) }
|
|
@@ -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,90 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
require 'test_helper'
|
|
4
|
+
require 'hexapdf/document'
|
|
5
|
+
require 'hexapdf/type/document_security_store'
|
|
6
|
+
|
|
7
|
+
describe HexaPDF::Type::DocumentSecurityStore do
|
|
8
|
+
before do
|
|
9
|
+
@doc = HexaPDF::Document.new
|
|
10
|
+
@dss = @doc.add({Type: :DSS})
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
[[:add_cert, :Certs], [:add_ocsp, :OCSPs], [:add_crl, :CRLs]].each do |method, field|
|
|
14
|
+
describe method do
|
|
15
|
+
it "adds a #{field[0..-2]} as an indirect stream object" do
|
|
16
|
+
result = @dss.send(method, "der_data")
|
|
17
|
+
assert_kind_of(HexaPDF::Stream, result)
|
|
18
|
+
assert_equal(:FlateDecode, result[:Filter])
|
|
19
|
+
assert_equal("der_data", result.stream)
|
|
20
|
+
assert_equal(1, @dss[field].size)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "returns the same stream object for duplicate #{field[0..-2]} data" do
|
|
24
|
+
result1 = @dss.send(method, "der_data")
|
|
25
|
+
result2 = @dss.send(method, "der_data")
|
|
26
|
+
assert_same(result1, result2)
|
|
27
|
+
assert_equal(1, @dss[field].size)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it "adds distinct stream objects for different #{field[0..-2]} data" do
|
|
31
|
+
@dss.send(method, "data_one")
|
|
32
|
+
@dss.send(method, "data_two")
|
|
33
|
+
assert_equal(2, @dss[field].size)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
describe "add_vri" do
|
|
39
|
+
before do
|
|
40
|
+
@signature_contents = 'signature bytes'
|
|
41
|
+
@signature = @doc.add({Type: :Sig, Contents: @signature_contents})
|
|
42
|
+
@vri_key = OpenSSL::Digest::SHA1.hexdigest(@signature_contents).upcase.to_sym
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it "returns the VRI entry" do
|
|
46
|
+
vri = @dss.add_vri(@signature)
|
|
47
|
+
assert_equal(:VRI, vri.type)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it "creates the /VRI dictionary with an entry for the signature" do
|
|
51
|
+
vri = @dss.add_vri(@signature)
|
|
52
|
+
assert(@dss.key?(:VRI))
|
|
53
|
+
assert_same(vri, @dss[:VRI][@vri_key])
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it "populates /Cert in the VRI entry and the DSS /Certs array" do
|
|
57
|
+
@dss.add_vri(@signature, certs: ["cert1", "cert2"])
|
|
58
|
+
assert_equal(2, @dss[:VRI][@vri_key][:Cert].size)
|
|
59
|
+
assert_equal(2, @dss[:Certs].size)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it "populates /OCSP in the VRI entry and the DSS /OCSPs array" do
|
|
63
|
+
@dss.add_vri(@signature, ocsps: ["ocsp1", "ocsp2"])
|
|
64
|
+
assert_equal(2, @dss[:VRI][@vri_key][:OCSP].size)
|
|
65
|
+
assert_equal(2, @dss[:OCSPs].size)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it "populates /CRL in the VRI entry and the DSS /CRLs array" do
|
|
69
|
+
@dss.add_vri(@signature, crls: ["crl1"])
|
|
70
|
+
assert_equal(1, @dss[:VRI][@vri_key][:CRL].size)
|
|
71
|
+
assert_equal(1, @dss[:CRLs].size)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it "de-duplicates DER streams shared across multiple VRI entries" do
|
|
75
|
+
@dss.add_vri(@signature, certs: ["shared_cert", "cert_a"])
|
|
76
|
+
@dss.add_vri(@doc.add({Type: :Sig, Contents: 'new'}), certs: ["shared_cert", "cert_b"])
|
|
77
|
+
# shared_cert added once despite appearing in both VRI entries
|
|
78
|
+
assert_equal(2, @dss[:VRI].value.size)
|
|
79
|
+
assert_equal(3, @dss[:Certs].size)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it "creates a VRI entry with no optional arrays when all inputs are empty" do
|
|
83
|
+
@dss.add_vri(@signature)
|
|
84
|
+
vri = @dss[:VRI][@vri_key]
|
|
85
|
+
refute(vri.key?(:Cert))
|
|
86
|
+
refute(vri.key?(:OCSP))
|
|
87
|
+
refute(vri.key?(:CRL))
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: hexapdf
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.10.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Thomas Leitner
|
|
@@ -874,6 +874,7 @@ files:
|
|
|
874
874
|
- test/hexapdf/type/test_annotation.rb
|
|
875
875
|
- test/hexapdf/type/test_catalog.rb
|
|
876
876
|
- test/hexapdf/type/test_cid_font.rb
|
|
877
|
+
- test/hexapdf/type/test_document_security_store.rb
|
|
877
878
|
- test/hexapdf/type/test_file_specification.rb
|
|
878
879
|
- test/hexapdf/type/test_font.rb
|
|
879
880
|
- test/hexapdf/type/test_font_descriptor.rb
|