pdf-reader 2.7.0 → 2.9.2
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 +20 -0
- data/Rakefile +1 -1
- data/lib/pdf/reader/aes_v2_security_handler.rb +41 -0
- data/lib/pdf/reader/aes_v3_security_handler.rb +38 -0
- data/lib/pdf/reader/bounding_rectangle_runs_filter.rb +16 -0
- data/lib/pdf/reader/buffer.rb +36 -34
- data/lib/pdf/reader/cmap.rb +64 -51
- data/lib/pdf/reader/error.rb +8 -0
- data/lib/pdf/reader/filter/ascii85.rb +1 -1
- data/lib/pdf/reader/filter/ascii_hex.rb +1 -1
- data/lib/pdf/reader/filter/depredict.rb +1 -1
- data/lib/pdf/reader/filter/flate.rb +3 -3
- data/lib/pdf/reader/filter/lzw.rb +1 -1
- data/lib/pdf/reader/filter/null.rb +1 -2
- data/lib/pdf/reader/filter/run_length.rb +1 -1
- data/lib/pdf/reader/filter.rb +10 -11
- data/lib/pdf/reader/font.rb +71 -16
- data/lib/pdf/reader/font_descriptor.rb +18 -17
- data/lib/pdf/reader/form_xobject.rb +14 -5
- data/lib/pdf/reader/key_builder_v5.rb +138 -0
- data/lib/pdf/reader/null_security_handler.rb +0 -4
- data/lib/pdf/reader/object_hash.rb +251 -44
- data/lib/pdf/reader/page.rb +51 -22
- data/lib/pdf/reader/page_layout.rb +14 -28
- data/lib/pdf/reader/page_state.rb +1 -1
- data/lib/pdf/reader/page_text_receiver.rb +52 -10
- data/lib/pdf/reader/parser.rb +22 -7
- data/lib/pdf/reader/point.rb +1 -1
- data/lib/pdf/reader/rc4_security_handler.rb +38 -0
- data/lib/pdf/reader/rectangle.rb +20 -2
- data/lib/pdf/reader/{resource_methods.rb → resources.rb} +15 -13
- data/lib/pdf/reader/security_handler_factory.rb +79 -0
- data/lib/pdf/reader/{standard_security_handler.rb → standard_key_builder.rb} +23 -95
- data/lib/pdf/reader/stream.rb +2 -2
- data/lib/pdf/reader/text_run.rb +13 -6
- data/lib/pdf/reader/type_check.rb +52 -0
- data/lib/pdf/reader/validating_receiver.rb +262 -0
- data/lib/pdf/reader/width_calculator/true_type.rb +1 -1
- data/lib/pdf/reader/xref.rb +20 -3
- data/lib/pdf/reader.rb +32 -11
- data/rbi/pdf-reader.rbi +408 -174
- metadata +16 -9
- data/lib/pdf/reader/standard_security_handler_v5.rb +0 -92
data/lib/pdf/reader/parser.rb
CHANGED
@@ -80,8 +80,8 @@ class PDF::Reader
|
|
80
80
|
token
|
81
81
|
elsif operators.has_key? token
|
82
82
|
Token.new(token)
|
83
|
-
elsif token.
|
84
|
-
token
|
83
|
+
elsif token.frozen?
|
84
|
+
token
|
85
85
|
elsif token =~ /\d*\.\d/
|
86
86
|
token.to_f
|
87
87
|
else
|
@@ -96,14 +96,20 @@ class PDF::Reader
|
|
96
96
|
# id - the object ID to return
|
97
97
|
# gen - the object revision number to return
|
98
98
|
def object(id, gen)
|
99
|
-
|
99
|
+
idCheck = parse_token
|
100
|
+
|
101
|
+
# Sometimes the xref table is corrupt and points to an offset slightly too early in the file.
|
102
|
+
# check the next token, maybe we can find the start of the object we're looking for
|
103
|
+
if idCheck != id
|
104
|
+
Error.assert_equal(parse_token, id)
|
105
|
+
end
|
100
106
|
Error.assert_equal(parse_token, gen)
|
101
107
|
Error.str_assert(parse_token, "obj")
|
102
108
|
|
103
109
|
obj = parse_token
|
104
110
|
post_obj = parse_token
|
105
111
|
|
106
|
-
if post_obj == "stream"
|
112
|
+
if obj.is_a?(Hash) && post_obj == "stream"
|
107
113
|
stream(obj)
|
108
114
|
else
|
109
115
|
obj
|
@@ -121,7 +127,7 @@ class PDF::Reader
|
|
121
127
|
key = parse_token
|
122
128
|
break if key.kind_of?(Token) and key == ">>"
|
123
129
|
raise MalformedPDFError, "unterminated dict" if @buffer.empty?
|
124
|
-
|
130
|
+
PDF::Reader::Error.validate_type_as_malformed(key, "Dictionary key", Symbol)
|
125
131
|
|
126
132
|
value = parse_token
|
127
133
|
value.kind_of?(Token) and Error.str_assert_not(value, ">>")
|
@@ -209,14 +215,23 @@ class PDF::Reader
|
|
209
215
|
def stream(dict)
|
210
216
|
raise MalformedPDFError, "PDF malformed, missing stream length" unless dict.has_key?(:Length)
|
211
217
|
if @objects
|
212
|
-
length = @objects.
|
218
|
+
length = @objects.deref_integer(dict[:Length])
|
219
|
+
if dict[:Filter]
|
220
|
+
dict[:Filter] = @objects.deref_name_or_array(dict[:Filter])
|
221
|
+
end
|
213
222
|
else
|
214
223
|
length = dict[:Length] || 0
|
215
224
|
end
|
225
|
+
|
226
|
+
PDF::Reader::Error.validate_type_as_malformed(length, "length", Numeric)
|
227
|
+
|
216
228
|
data = @buffer.read(length, :skip_eol => true)
|
217
229
|
|
218
230
|
Error.str_assert(parse_token, "endstream")
|
219
|
-
|
231
|
+
|
232
|
+
# We used to assert that the stream had the correct closing token, but it doesn't *really*
|
233
|
+
# matter if it's missing, and other readers seems to handle its absence just fine
|
234
|
+
# Error.str_assert(parse_token, "endobj")
|
220
235
|
|
221
236
|
PDF::Reader::Stream.new(dict, data)
|
222
237
|
end
|
data/lib/pdf/reader/point.rb
CHANGED
@@ -0,0 +1,38 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
# typed: strict
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
require 'digest/md5'
|
6
|
+
require 'rc4'
|
7
|
+
|
8
|
+
class PDF::Reader
|
9
|
+
|
10
|
+
# Decrypts data using the RC4 algorithim defined in the PDF spec. Requires
|
11
|
+
# a decryption key, which is usually generated by PDF::Reader::StandardKeyBuilder
|
12
|
+
#
|
13
|
+
class Rc4SecurityHandler
|
14
|
+
|
15
|
+
def initialize(key)
|
16
|
+
@encrypt_key = key
|
17
|
+
end
|
18
|
+
|
19
|
+
##7.6.2 General Encryption Algorithm
|
20
|
+
#
|
21
|
+
# Algorithm 1: Encryption of data using the RC4 algorithm
|
22
|
+
#
|
23
|
+
# version <=3 or (version == 4 and CFM == V2)
|
24
|
+
#
|
25
|
+
# buf - a string to decrypt
|
26
|
+
# ref - a PDF::Reader::Reference for the object to decrypt
|
27
|
+
#
|
28
|
+
def decrypt( buf, ref )
|
29
|
+
objKey = @encrypt_key.dup
|
30
|
+
(0..2).each { |e| objKey << (ref.id >> e*8 & 0xFF ) }
|
31
|
+
(0..1).each { |e| objKey << (ref.gen >> e*8 & 0xFF ) }
|
32
|
+
length = objKey.length < 16 ? objKey.length : 16
|
33
|
+
rc4 = RC4.new( Digest::MD5.digest(objKey)[0,length] )
|
34
|
+
rc4.decrypt(buf)
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
data/lib/pdf/reader/rectangle.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
# coding: utf-8
|
2
|
-
# typed:
|
2
|
+
# typed: strict
|
3
3
|
# frozen_string_literal: true
|
4
4
|
|
5
5
|
module PDF
|
@@ -26,6 +26,19 @@ module PDF
|
|
26
26
|
set_corners(x1, y1, x2, y2)
|
27
27
|
end
|
28
28
|
|
29
|
+
def self.from_array(arr)
|
30
|
+
if arr.size != 4
|
31
|
+
raise ArgumentError, "Only 4-element Arrays can be converted to a Rectangle"
|
32
|
+
end
|
33
|
+
|
34
|
+
PDF::Reader::Rectangle.new(
|
35
|
+
arr[0].to_f,
|
36
|
+
arr[1].to_f,
|
37
|
+
arr[2].to_f,
|
38
|
+
arr[3].to_f,
|
39
|
+
)
|
40
|
+
end
|
41
|
+
|
29
42
|
def ==(other)
|
30
43
|
to_a == other.to_a
|
31
44
|
end
|
@@ -38,6 +51,11 @@ module PDF
|
|
38
51
|
bottom_right.x - bottom_left.x
|
39
52
|
end
|
40
53
|
|
54
|
+
def contains?(point)
|
55
|
+
point.x >= bottom_left.x && point.x <= top_right.x &&
|
56
|
+
point.y >= bottom_left.y && point.y <= top_right.y
|
57
|
+
end
|
58
|
+
|
41
59
|
# A pdf-style 4-number array
|
42
60
|
def to_a
|
43
61
|
[
|
@@ -67,7 +85,7 @@ module PDF
|
|
67
85
|
new_x2 = bottom_left.x
|
68
86
|
new_y2 = bottom_left.y + width
|
69
87
|
end
|
70
|
-
set_corners(new_x1, new_y1, new_x2, new_y2)
|
88
|
+
set_corners(new_x1 || 0, new_y1 || 0, new_x2 || 0, new_y2 || 0)
|
71
89
|
end
|
72
90
|
|
73
91
|
private
|
@@ -1,16 +1,18 @@
|
|
1
1
|
# coding: utf-8
|
2
|
-
# typed:
|
2
|
+
# typed: true
|
3
3
|
# frozen_string_literal: true
|
4
4
|
|
5
|
-
# Setting this file to "typed: true" is difficult because it's a mixin that assumes some things
|
6
|
-
# are aavailable from the class, like @objects and resources. Sorbet doesn't know about them.
|
7
|
-
|
8
5
|
module PDF
|
9
6
|
class Reader
|
10
7
|
|
11
8
|
# mixin for common methods in Page and FormXobjects
|
12
9
|
#
|
13
|
-
|
10
|
+
class Resources
|
11
|
+
|
12
|
+
def initialize(objects, resources)
|
13
|
+
@objects = objects
|
14
|
+
@resources = resources
|
15
|
+
end
|
14
16
|
|
15
17
|
# Returns a Hash of color spaces that are available to this page
|
16
18
|
#
|
@@ -19,7 +21,7 @@ module PDF
|
|
19
21
|
# of calling it over and over.
|
20
22
|
#
|
21
23
|
def color_spaces
|
22
|
-
@objects.
|
24
|
+
@objects.deref_hash!(@resources[:ColorSpace]) || {}
|
23
25
|
end
|
24
26
|
|
25
27
|
# Returns a Hash of fonts that are available to this page
|
@@ -29,7 +31,7 @@ module PDF
|
|
29
31
|
# of calling it over and over.
|
30
32
|
#
|
31
33
|
def fonts
|
32
|
-
@objects.
|
34
|
+
@objects.deref_hash!(@resources[:Font]) || {}
|
33
35
|
end
|
34
36
|
|
35
37
|
# Returns a Hash of external graphic states that are available to this
|
@@ -40,7 +42,7 @@ module PDF
|
|
40
42
|
# of calling it over and over.
|
41
43
|
#
|
42
44
|
def graphic_states
|
43
|
-
@objects.
|
45
|
+
@objects.deref_hash!(@resources[:ExtGState]) || {}
|
44
46
|
end
|
45
47
|
|
46
48
|
# Returns a Hash of patterns that are available to this page
|
@@ -50,7 +52,7 @@ module PDF
|
|
50
52
|
# of calling it over and over.
|
51
53
|
#
|
52
54
|
def patterns
|
53
|
-
@objects.
|
55
|
+
@objects.deref_hash!(@resources[:Pattern]) || {}
|
54
56
|
end
|
55
57
|
|
56
58
|
# Returns an Array of procedure sets that are available to this page
|
@@ -60,7 +62,7 @@ module PDF
|
|
60
62
|
# of calling it over and over.
|
61
63
|
#
|
62
64
|
def procedure_sets
|
63
|
-
@objects.
|
65
|
+
@objects.deref_array!(@resources[:ProcSet]) || []
|
64
66
|
end
|
65
67
|
|
66
68
|
# Returns a Hash of properties sets that are available to this page
|
@@ -70,7 +72,7 @@ module PDF
|
|
70
72
|
# of calling it over and over.
|
71
73
|
#
|
72
74
|
def properties
|
73
|
-
@objects.
|
75
|
+
@objects.deref_hash!(@resources[:Properties]) || {}
|
74
76
|
end
|
75
77
|
|
76
78
|
# Returns a Hash of shadings that are available to this page
|
@@ -80,7 +82,7 @@ module PDF
|
|
80
82
|
# of calling it over and over.
|
81
83
|
#
|
82
84
|
def shadings
|
83
|
-
@objects.
|
85
|
+
@objects.deref_hash!(@resources[:Shading]) || {}
|
84
86
|
end
|
85
87
|
|
86
88
|
# Returns a Hash of XObjects that are available to this page
|
@@ -90,7 +92,7 @@ module PDF
|
|
90
92
|
# of calling it over and over.
|
91
93
|
#
|
92
94
|
def xobjects
|
93
|
-
@objects.
|
95
|
+
@objects.deref_hash!(@resources[:XObject]) || {}
|
94
96
|
end
|
95
97
|
|
96
98
|
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
# typed: strict
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
class PDF::Reader
|
6
|
+
# Examines the Encrypt entry of a PDF trailer (if any) and returns an object that's
|
7
|
+
# able to decrypt the file.
|
8
|
+
class SecurityHandlerFactory
|
9
|
+
|
10
|
+
def self.build(encrypt, doc_id, password)
|
11
|
+
doc_id ||= []
|
12
|
+
password ||= ""
|
13
|
+
|
14
|
+
if encrypt.nil?
|
15
|
+
NullSecurityHandler.new
|
16
|
+
elsif standard?(encrypt)
|
17
|
+
build_standard_handler(encrypt, doc_id, password)
|
18
|
+
elsif standard_v5?(encrypt)
|
19
|
+
build_v5_handler(encrypt, doc_id, password)
|
20
|
+
else
|
21
|
+
UnimplementedSecurityHandler.new
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.build_standard_handler(encrypt, doc_id, password)
|
26
|
+
encmeta = !encrypt.has_key?(:EncryptMetadata) || encrypt[:EncryptMetadata].to_s == "true"
|
27
|
+
key_builder = StandardKeyBuilder.new(
|
28
|
+
key_length: (encrypt[:Length] || 40).to_i,
|
29
|
+
revision: encrypt[:R],
|
30
|
+
owner_key: encrypt[:O],
|
31
|
+
user_key: encrypt[:U],
|
32
|
+
permissions: encrypt[:P].to_i,
|
33
|
+
encrypted_metadata: encmeta,
|
34
|
+
file_id: doc_id.first,
|
35
|
+
)
|
36
|
+
cfm = encrypt.fetch(:CF, {}).fetch(encrypt[:StmF], {}).fetch(:CFM, nil)
|
37
|
+
if cfm == :AESV2
|
38
|
+
AesV2SecurityHandler.new(key_builder.key(password))
|
39
|
+
else
|
40
|
+
Rc4SecurityHandler.new(key_builder.key(password))
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.build_v5_handler(encrypt, doc_id, password)
|
45
|
+
key_builder = KeyBuilderV5.new(
|
46
|
+
owner_key: encrypt[:O],
|
47
|
+
user_key: encrypt[:U],
|
48
|
+
owner_encryption_key: encrypt[:OE],
|
49
|
+
user_encryption_key: encrypt[:UE],
|
50
|
+
)
|
51
|
+
AesV3SecurityHandler.new(key_builder.key(password))
|
52
|
+
end
|
53
|
+
|
54
|
+
# This handler supports all encryption that follows upto PDF 1.5 spec (revision 4)
|
55
|
+
def self.standard?(encrypt)
|
56
|
+
return false if encrypt.nil?
|
57
|
+
|
58
|
+
filter = encrypt.fetch(:Filter, :Standard)
|
59
|
+
version = encrypt.fetch(:V, 0)
|
60
|
+
algorithm = encrypt.fetch(:CF, {}).fetch(encrypt[:StmF], {}).fetch(:CFM, nil)
|
61
|
+
(filter == :Standard) && (encrypt[:StmF] == encrypt[:StrF]) &&
|
62
|
+
(version <= 3 || (version == 4 && ((algorithm == :V2) || (algorithm == :AESV2))))
|
63
|
+
end
|
64
|
+
|
65
|
+
# This handler supports both
|
66
|
+
# - AES-256 encryption defined in PDF 1.7 Extension Level 3 ('revision 5')
|
67
|
+
# - AES-256 encryption defined in PDF 2.0 ('revision 6')
|
68
|
+
def self.standard_v5?(encrypt)
|
69
|
+
return false if encrypt.nil?
|
70
|
+
|
71
|
+
filter = encrypt.fetch(:Filter, :Standard)
|
72
|
+
version = encrypt.fetch(:V, 0)
|
73
|
+
revision = encrypt.fetch(:R, 0)
|
74
|
+
algorithm = encrypt.fetch(:CF, {}).fetch(encrypt[:StmF], {}).fetch(:CFM, nil)
|
75
|
+
(filter == :Standard) && (encrypt[:StmF] == encrypt[:StrF]) &&
|
76
|
+
((version == 5) && (revision == 5 || revision == 6) && (algorithm == :AESV3))
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -1,39 +1,19 @@
|
|
1
1
|
# coding: utf-8
|
2
|
-
# typed: true
|
3
|
-
# frozen_string_literal: true
|
4
2
|
|
5
|
-
################################################################################
|
6
|
-
#
|
7
|
-
# Copyright (C) 2011 Evan J Brunner (ejbrun@appittome.com)
|
8
|
-
#
|
9
|
-
# Permission is hereby granted, free of charge, to any person obtaining
|
10
|
-
# a copy of this software and associated documentation files (the
|
11
|
-
# "Software"), to deal in the Software without restriction, including
|
12
|
-
# without limitation the rights to use, copy, modify, merge, publish,
|
13
|
-
# distribute, sublicense, and/or sell copies of the Software, and to
|
14
|
-
# permit persons to whom the Software is furnished to do so, subject to
|
15
|
-
# the following conditions:
|
16
|
-
#
|
17
|
-
# The above copyright notice and this permission notice shall be
|
18
|
-
# included in all copies or substantial portions of the Software.
|
19
|
-
#
|
20
|
-
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
21
|
-
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
22
|
-
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
23
|
-
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
24
|
-
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
25
|
-
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
26
|
-
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
27
|
-
#
|
28
|
-
################################################################################
|
29
3
|
require 'digest/md5'
|
30
|
-
require 'openssl'
|
31
4
|
require 'rc4'
|
32
5
|
|
33
6
|
class PDF::Reader
|
34
7
|
|
35
|
-
#
|
36
|
-
|
8
|
+
# Processes the Encrypt dict from an encrypted PDF and a user provided
|
9
|
+
# password and returns a key that can decrypt the file.
|
10
|
+
#
|
11
|
+
# This can generate a key compatible with the following standard encryption algorithms:
|
12
|
+
#
|
13
|
+
# * Version 1-3, all variants
|
14
|
+
# * Version 4, V2 (RC4) and AESV2
|
15
|
+
#
|
16
|
+
class StandardKeyBuilder
|
37
17
|
|
38
18
|
## 7.6.3.3 Encryption Key Algorithm (pp61)
|
39
19
|
#
|
@@ -45,9 +25,6 @@ class PDF::Reader
|
|
45
25
|
0x2e, 0x2e, 0x00, 0xb6, 0xd0, 0x68, 0x3e, 0x80,
|
46
26
|
0x2f, 0x0c, 0xa9, 0xfe, 0x64, 0x53, 0x69, 0x7a ]
|
47
27
|
|
48
|
-
attr_reader :key_length, :revision, :encrypt_key
|
49
|
-
attr_reader :owner_key, :user_key, :permissions, :file_id, :password
|
50
|
-
|
51
28
|
def initialize(opts = {})
|
52
29
|
@key_length = opts[:key_length].to_i/8
|
53
30
|
@revision = opts[:revision].to_i
|
@@ -56,72 +33,30 @@ class PDF::Reader
|
|
56
33
|
@permissions = opts[:permissions].to_i
|
57
34
|
@encryptMeta = opts.fetch(:encrypted_metadata, true)
|
58
35
|
@file_id = opts[:file_id] || ""
|
59
|
-
@encrypt_key = build_standard_key(opts[:password] || "")
|
60
|
-
@cfm = opts[:cfm]
|
61
36
|
|
62
37
|
if @key_length != 5 && @key_length != 16
|
63
|
-
msg = "
|
38
|
+
msg = "StandardKeyBuilder only supports 40 and 128 bit\
|
64
39
|
encryption (#{@key_length * 8}bit)"
|
65
|
-
raise
|
40
|
+
raise UnsupportedFeatureError, msg
|
66
41
|
end
|
67
42
|
end
|
68
43
|
|
69
|
-
#
|
70
|
-
def self.supports?(encrypt)
|
71
|
-
return false if encrypt.nil?
|
72
|
-
|
73
|
-
filter = encrypt.fetch(:Filter, :Standard)
|
74
|
-
version = encrypt.fetch(:V, 0)
|
75
|
-
algorithm = encrypt.fetch(:CF, {}).fetch(encrypt[:StmF], {}).fetch(:CFM, nil)
|
76
|
-
(filter == :Standard) && (encrypt[:StmF] == encrypt[:StrF]) &&
|
77
|
-
(version <= 3 || (version == 4 && ((algorithm == :V2) || (algorithm == :AESV2))))
|
78
|
-
end
|
79
|
-
|
80
|
-
##7.6.2 General Encryption Algorithm
|
81
|
-
#
|
82
|
-
# Algorithm 1: Encryption of data using the RC4 or AES algorithms
|
83
|
-
#
|
84
|
-
# used to decrypt RC4/AES encrypted PDF streams (buf)
|
44
|
+
# Takes a string containing a user provided password.
|
85
45
|
#
|
86
|
-
#
|
87
|
-
#
|
46
|
+
# If the password matches the file, then a string containing a key suitable for
|
47
|
+
# decrypting the file will be returned. If the password doesn't match the file,
|
48
|
+
# and exception will be raised.
|
88
49
|
#
|
89
|
-
def
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
else
|
94
|
-
decrypt_rc4(buf, ref)
|
95
|
-
end
|
96
|
-
end
|
97
|
-
|
98
|
-
private
|
50
|
+
def key(pass)
|
51
|
+
pass ||= ""
|
52
|
+
encrypt_key = auth_owner_pass(pass)
|
53
|
+
encrypt_key ||= auth_user_pass(pass)
|
99
54
|
|
100
|
-
|
101
|
-
|
102
|
-
def decrypt_rc4( buf, ref )
|
103
|
-
objKey = @encrypt_key.dup
|
104
|
-
(0..2).each { |e| objKey << (ref.id >> e*8 & 0xFF ) }
|
105
|
-
(0..1).each { |e| objKey << (ref.gen >> e*8 & 0xFF ) }
|
106
|
-
length = objKey.length < 16 ? objKey.length : 16
|
107
|
-
rc4 = RC4.new( Digest::MD5.digest(objKey)[0,length] )
|
108
|
-
rc4.decrypt(buf)
|
55
|
+
raise PDF::Reader::EncryptedPDFError, "Invalid password (#{pass})" if encrypt_key.nil?
|
56
|
+
encrypt_key
|
109
57
|
end
|
110
58
|
|
111
|
-
|
112
|
-
# when (version == 4 and CFM == AESV2)
|
113
|
-
def decrypt_aes128( buf, ref )
|
114
|
-
objKey = @encrypt_key.dup
|
115
|
-
(0..2).each { |e| objKey << (ref.id >> e*8 & 0xFF ) }
|
116
|
-
(0..1).each { |e| objKey << (ref.gen >> e*8 & 0xFF ) }
|
117
|
-
objKey << 'sAlT' # Algorithm 1, b)
|
118
|
-
length = objKey.length < 16 ? objKey.length : 16
|
119
|
-
cipher = OpenSSL::Cipher.new("AES-#{length << 3}-CBC")
|
120
|
-
cipher.decrypt
|
121
|
-
cipher.key = Digest::MD5.digest(objKey)[0,length]
|
122
|
-
cipher.iv = buf[0..15]
|
123
|
-
cipher.update(buf[16..-1]) + cipher.final
|
124
|
-
end
|
59
|
+
private
|
125
60
|
|
126
61
|
# Pads supplied password to 32bytes using PassPadBytes as specified on
|
127
62
|
# pp61 of spec
|
@@ -153,7 +88,7 @@ class PDF::Reader
|
|
153
88
|
md5 = Digest::MD5.digest(pad_pass(pass))
|
154
89
|
if @revision > 2 then
|
155
90
|
50.times { md5 = Digest::MD5.digest(md5) }
|
156
|
-
keyBegins = md5[0, key_length]
|
91
|
+
keyBegins = md5[0, @key_length]
|
157
92
|
#first iteration decrypt owner_key
|
158
93
|
out = @owner_key
|
159
94
|
#RC4 keyed with (keyBegins XOR with iteration #) to decrypt previous out
|
@@ -218,12 +153,5 @@ class PDF::Reader
|
|
218
153
|
end
|
219
154
|
end
|
220
155
|
|
221
|
-
def build_standard_key(pass)
|
222
|
-
encrypt_key = auth_owner_pass(pass)
|
223
|
-
encrypt_key ||= auth_user_pass(pass)
|
224
|
-
|
225
|
-
raise PDF::Reader::EncryptedPDFError, "Invalid password (#{pass})" if encrypt_key.nil?
|
226
|
-
encrypt_key
|
227
|
-
end
|
228
156
|
end
|
229
157
|
end
|
data/lib/pdf/reader/stream.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
# coding: utf-8
|
2
|
-
# typed:
|
2
|
+
# typed: strict
|
3
3
|
# frozen_string_literal: true
|
4
4
|
|
5
5
|
################################################################################
|
@@ -62,7 +62,7 @@ class PDF::Reader
|
|
62
62
|
end
|
63
63
|
|
64
64
|
Array(hash[:Filter]).each_with_index do |filter, index|
|
65
|
-
@udata = Filter.with(filter, options[index]).filter(@udata)
|
65
|
+
@udata = Filter.with(filter, options[index] || {}).filter(@udata)
|
66
66
|
end
|
67
67
|
end
|
68
68
|
@udata
|
data/lib/pdf/reader/text_run.rb
CHANGED
@@ -7,15 +7,14 @@ class PDF::Reader
|
|
7
7
|
class TextRun
|
8
8
|
include Comparable
|
9
9
|
|
10
|
-
attr_reader :
|
10
|
+
attr_reader :origin, :width, :font_size, :text
|
11
11
|
|
12
12
|
alias :to_s :text
|
13
13
|
|
14
14
|
def initialize(x, y, width, font_size, text)
|
15
|
-
@
|
16
|
-
@y = y
|
15
|
+
@origin = PDF::Reader::Point.new(x, y)
|
17
16
|
@width = width
|
18
|
-
@font_size = font_size
|
17
|
+
@font_size = font_size
|
19
18
|
@text = text
|
20
19
|
end
|
21
20
|
|
@@ -35,12 +34,20 @@ class PDF::Reader
|
|
35
34
|
end
|
36
35
|
end
|
37
36
|
|
37
|
+
def x
|
38
|
+
@origin.x
|
39
|
+
end
|
40
|
+
|
41
|
+
def y
|
42
|
+
@origin.y
|
43
|
+
end
|
44
|
+
|
38
45
|
def endx
|
39
|
-
@endx ||= x + width
|
46
|
+
@endx ||= @origin.x + width
|
40
47
|
end
|
41
48
|
|
42
49
|
def endy
|
43
|
-
@endy ||= y + font_size
|
50
|
+
@endy ||= @origin.y + font_size
|
44
51
|
end
|
45
52
|
|
46
53
|
def mean_character_width
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
# typed: strict
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
module PDF
|
6
|
+
class Reader
|
7
|
+
|
8
|
+
# Cast untrusted input (usually parsed out of a PDF file) to a known type
|
9
|
+
#
|
10
|
+
class TypeCheck
|
11
|
+
|
12
|
+
def self.cast_to_numeric!(obj)
|
13
|
+
if obj.is_a?(Numeric)
|
14
|
+
obj
|
15
|
+
elsif obj.nil?
|
16
|
+
0
|
17
|
+
elsif obj.respond_to?(:to_f)
|
18
|
+
obj.to_f
|
19
|
+
elsif obj.respond_to?(:to_i)
|
20
|
+
obj.to_i
|
21
|
+
else
|
22
|
+
raise MalformedPDFError, "Unable to cast to numeric"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.cast_to_string!(string)
|
27
|
+
if string.is_a?(String)
|
28
|
+
string
|
29
|
+
elsif string.nil?
|
30
|
+
""
|
31
|
+
elsif string.respond_to?(:to_s)
|
32
|
+
string.to_s
|
33
|
+
else
|
34
|
+
raise MalformedPDFError, "Unable to cast to string"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.cast_to_symbol(obj)
|
39
|
+
if obj.is_a?(Symbol)
|
40
|
+
obj
|
41
|
+
elsif obj.nil?
|
42
|
+
nil
|
43
|
+
elsif obj.respond_to?(:to_sym)
|
44
|
+
obj.to_sym
|
45
|
+
else
|
46
|
+
raise MalformedPDFError, "Unable to cast to symbol"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|