pdf-reader 2.7.0 → 2.9.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +20 -0
  3. data/Rakefile +1 -1
  4. data/lib/pdf/reader/aes_v2_security_handler.rb +41 -0
  5. data/lib/pdf/reader/aes_v3_security_handler.rb +38 -0
  6. data/lib/pdf/reader/bounding_rectangle_runs_filter.rb +16 -0
  7. data/lib/pdf/reader/buffer.rb +36 -34
  8. data/lib/pdf/reader/cmap.rb +64 -51
  9. data/lib/pdf/reader/error.rb +8 -0
  10. data/lib/pdf/reader/filter/ascii85.rb +1 -1
  11. data/lib/pdf/reader/filter/ascii_hex.rb +1 -1
  12. data/lib/pdf/reader/filter/depredict.rb +1 -1
  13. data/lib/pdf/reader/filter/flate.rb +3 -3
  14. data/lib/pdf/reader/filter/lzw.rb +1 -1
  15. data/lib/pdf/reader/filter/null.rb +1 -2
  16. data/lib/pdf/reader/filter/run_length.rb +1 -1
  17. data/lib/pdf/reader/filter.rb +10 -11
  18. data/lib/pdf/reader/font.rb +71 -16
  19. data/lib/pdf/reader/font_descriptor.rb +18 -17
  20. data/lib/pdf/reader/form_xobject.rb +14 -5
  21. data/lib/pdf/reader/key_builder_v5.rb +138 -0
  22. data/lib/pdf/reader/null_security_handler.rb +0 -4
  23. data/lib/pdf/reader/object_hash.rb +251 -44
  24. data/lib/pdf/reader/page.rb +51 -22
  25. data/lib/pdf/reader/page_layout.rb +14 -28
  26. data/lib/pdf/reader/page_state.rb +1 -1
  27. data/lib/pdf/reader/page_text_receiver.rb +52 -10
  28. data/lib/pdf/reader/parser.rb +22 -7
  29. data/lib/pdf/reader/point.rb +1 -1
  30. data/lib/pdf/reader/rc4_security_handler.rb +38 -0
  31. data/lib/pdf/reader/rectangle.rb +20 -2
  32. data/lib/pdf/reader/{resource_methods.rb → resources.rb} +15 -13
  33. data/lib/pdf/reader/security_handler_factory.rb +79 -0
  34. data/lib/pdf/reader/{standard_security_handler.rb → standard_key_builder.rb} +23 -95
  35. data/lib/pdf/reader/stream.rb +2 -2
  36. data/lib/pdf/reader/text_run.rb +13 -6
  37. data/lib/pdf/reader/type_check.rb +52 -0
  38. data/lib/pdf/reader/validating_receiver.rb +262 -0
  39. data/lib/pdf/reader/width_calculator/true_type.rb +1 -1
  40. data/lib/pdf/reader/xref.rb +20 -3
  41. data/lib/pdf/reader.rb +32 -11
  42. data/rbi/pdf-reader.rbi +408 -174
  43. metadata +16 -9
  44. data/lib/pdf/reader/standard_security_handler_v5.rb +0 -92
@@ -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.respond_to?(:to_token)
84
- token.to_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
- Error.assert_equal(parse_token, id)
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
- raise MalformedPDFError, "Dictionary key (#{key.inspect}) is not a name" unless key.kind_of?(Symbol)
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.deref(dict[:Length])
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
- Error.str_assert(parse_token, "endobj")
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
@@ -1,5 +1,5 @@
1
1
  # coding: utf-8
2
- # typed: true
2
+ # typed: strict
3
3
  # frozen_string_literal: true
4
4
 
5
5
  module PDF
@@ -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
@@ -1,5 +1,5 @@
1
1
  # coding: utf-8
2
- # typed: true
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: false
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
- module ResourceMethods
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.deref!(resources[:ColorSpace]) || {}
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.deref!(resources[:Font]) || {}
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.deref!(resources[:ExtGState]) || {}
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.deref!(resources[:Pattern]) || {}
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.deref!(resources[:ProcSet]) || []
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.deref!(resources[:Properties]) || {}
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.deref!(resources[:Shading]) || {}
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.deref!(resources[:XObject]) || {}
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
- # class creates interface to encrypt dictionary for use in Decrypt
36
- class StandardSecurityHandler
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 = "StandardSecurityHandler only supports 40 and 128 bit\
38
+ msg = "StandardKeyBuilder only supports 40 and 128 bit\
64
39
  encryption (#{@key_length * 8}bit)"
65
- raise ArgumentError, msg
40
+ raise UnsupportedFeatureError, msg
66
41
  end
67
42
  end
68
43
 
69
- # This handler supports all encryption that follows upto PDF 1.5 spec (revision 4)
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
- # buf - a string to decrypt
87
- # ref - a PDF::Reader::Reference for the object to decrypt
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 decrypt( buf, ref )
90
- case @cfm
91
- when :AESV2
92
- decrypt_aes128(buf, ref)
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
- # decrypt with RC4 algorithm
101
- # version <=3 or (version == 4 and CFM == V2)
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
- # decrypt with AES-128-CBC algorithm
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
@@ -1,5 +1,5 @@
1
1
  # coding: utf-8
2
- # typed: true
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
@@ -7,15 +7,14 @@ class PDF::Reader
7
7
  class TextRun
8
8
  include Comparable
9
9
 
10
- attr_reader :x, :y, :width, :font_size, :text
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
- @x = x
16
- @y = y
15
+ @origin = PDF::Reader::Point.new(x, y)
17
16
  @width = width
18
- @font_size = font_size.floor
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
+