pdf-reader 2.5.0 → 2.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +42 -0
  3. data/README.md +16 -1
  4. data/Rakefile +1 -1
  5. data/examples/extract_fonts.rb +12 -7
  6. data/examples/rspec.rb +1 -0
  7. data/lib/pdf/reader/aes_v2_security_handler.rb +41 -0
  8. data/lib/pdf/reader/aes_v3_security_handler.rb +38 -0
  9. data/lib/pdf/reader/bounding_rectangle_runs_filter.rb +16 -0
  10. data/lib/pdf/reader/buffer.rb +90 -46
  11. data/lib/pdf/reader/cid_widths.rb +1 -0
  12. data/lib/pdf/reader/cmap.rb +65 -50
  13. data/lib/pdf/reader/encoding.rb +3 -2
  14. data/lib/pdf/reader/error.rb +19 -3
  15. data/lib/pdf/reader/filter/ascii85.rb +7 -1
  16. data/lib/pdf/reader/filter/ascii_hex.rb +6 -1
  17. data/lib/pdf/reader/filter/depredict.rb +11 -9
  18. data/lib/pdf/reader/filter/flate.rb +4 -2
  19. data/lib/pdf/reader/filter/lzw.rb +2 -0
  20. data/lib/pdf/reader/filter/null.rb +1 -1
  21. data/lib/pdf/reader/filter/run_length.rb +19 -13
  22. data/lib/pdf/reader/filter.rb +2 -1
  23. data/lib/pdf/reader/font.rb +72 -16
  24. data/lib/pdf/reader/font_descriptor.rb +19 -17
  25. data/lib/pdf/reader/form_xobject.rb +15 -5
  26. data/lib/pdf/reader/glyph_hash.rb +16 -9
  27. data/lib/pdf/reader/glyphlist-zapfdingbats.txt +245 -0
  28. data/lib/pdf/reader/key_builder_v5.rb +138 -0
  29. data/lib/pdf/reader/lzw.rb +4 -2
  30. data/lib/pdf/reader/null_security_handler.rb +1 -4
  31. data/lib/pdf/reader/object_cache.rb +1 -0
  32. data/lib/pdf/reader/object_hash.rb +252 -44
  33. data/lib/pdf/reader/object_stream.rb +1 -0
  34. data/lib/pdf/reader/overlapping_runs_filter.rb +11 -4
  35. data/lib/pdf/reader/page.rb +99 -19
  36. data/lib/pdf/reader/page_layout.rb +36 -37
  37. data/lib/pdf/reader/page_state.rb +12 -11
  38. data/lib/pdf/reader/page_text_receiver.rb +57 -10
  39. data/lib/pdf/reader/pages_strategy.rb +1 -0
  40. data/lib/pdf/reader/parser.rb +23 -12
  41. data/lib/pdf/reader/point.rb +25 -0
  42. data/lib/pdf/reader/print_receiver.rb +1 -0
  43. data/lib/pdf/reader/rc4_security_handler.rb +38 -0
  44. data/lib/pdf/reader/rectangle.rb +113 -0
  45. data/lib/pdf/reader/reference.rb +1 -0
  46. data/lib/pdf/reader/register_receiver.rb +1 -0
  47. data/lib/pdf/reader/{resource_methods.rb → resources.rb} +16 -9
  48. data/lib/pdf/reader/security_handler_factory.rb +79 -0
  49. data/lib/pdf/reader/{standard_security_handler.rb → standard_key_builder.rb} +23 -94
  50. data/lib/pdf/reader/stream.rb +2 -1
  51. data/lib/pdf/reader/synchronized_cache.rb +1 -0
  52. data/lib/pdf/reader/text_run.rb +14 -6
  53. data/lib/pdf/reader/token.rb +1 -0
  54. data/lib/pdf/reader/transformation_matrix.rb +1 -0
  55. data/lib/pdf/reader/type_check.rb +52 -0
  56. data/lib/pdf/reader/unimplemented_security_handler.rb +1 -0
  57. data/lib/pdf/reader/validating_receiver.rb +262 -0
  58. data/lib/pdf/reader/width_calculator/built_in.rb +1 -0
  59. data/lib/pdf/reader/width_calculator/composite.rb +1 -0
  60. data/lib/pdf/reader/width_calculator/true_type.rb +2 -1
  61. data/lib/pdf/reader/width_calculator/type_one_or_three.rb +1 -0
  62. data/lib/pdf/reader/width_calculator/type_zero.rb +1 -0
  63. data/lib/pdf/reader/width_calculator.rb +1 -0
  64. data/lib/pdf/reader/xref.rb +27 -4
  65. data/lib/pdf/reader/zero_width_runs_filter.rb +13 -0
  66. data/lib/pdf/reader.rb +46 -15
  67. data/lib/pdf-reader.rb +1 -0
  68. data/rbi/pdf-reader.rbi +1978 -0
  69. metadata +21 -10
  70. data/lib/pdf/reader/orientation_detector.rb +0 -34
  71. data/lib/pdf/reader/standard_security_handler_v5.rb +0 -91
@@ -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,38 +1,19 @@
1
1
  # coding: utf-8
2
- # frozen_string_literal: true
3
2
 
4
- ################################################################################
5
- #
6
- # Copyright (C) 2011 Evan J Brunner (ejbrun@appittome.com)
7
- #
8
- # Permission is hereby granted, free of charge, to any person obtaining
9
- # a copy of this software and associated documentation files (the
10
- # "Software"), to deal in the Software without restriction, including
11
- # without limitation the rights to use, copy, modify, merge, publish,
12
- # distribute, sublicense, and/or sell copies of the Software, and to
13
- # permit persons to whom the Software is furnished to do so, subject to
14
- # the following conditions:
15
- #
16
- # The above copyright notice and this permission notice shall be
17
- # included in all copies or substantial portions of the Software.
18
- #
19
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
20
- # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
21
- # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
22
- # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
23
- # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
24
- # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
25
- # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
26
- #
27
- ################################################################################
28
3
  require 'digest/md5'
29
- require 'openssl'
30
4
  require 'rc4'
31
5
 
32
6
  class PDF::Reader
33
7
 
34
- # class creates interface to encrypt dictionary for use in Decrypt
35
- 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
36
17
 
37
18
  ## 7.6.3.3 Encryption Key Algorithm (pp61)
38
19
  #
@@ -44,9 +25,6 @@ class PDF::Reader
44
25
  0x2e, 0x2e, 0x00, 0xb6, 0xd0, 0x68, 0x3e, 0x80,
45
26
  0x2f, 0x0c, 0xa9, 0xfe, 0x64, 0x53, 0x69, 0x7a ]
46
27
 
47
- attr_reader :key_length, :revision, :encrypt_key
48
- attr_reader :owner_key, :user_key, :permissions, :file_id, :password
49
-
50
28
  def initialize(opts = {})
51
29
  @key_length = opts[:key_length].to_i/8
52
30
  @revision = opts[:revision].to_i
@@ -55,72 +33,30 @@ class PDF::Reader
55
33
  @permissions = opts[:permissions].to_i
56
34
  @encryptMeta = opts.fetch(:encrypted_metadata, true)
57
35
  @file_id = opts[:file_id] || ""
58
- @encrypt_key = build_standard_key(opts[:password] || "")
59
- @cfm = opts[:cfm]
60
36
 
61
37
  if @key_length != 5 && @key_length != 16
62
- msg = "StandardSecurityHandler only supports 40 and 128 bit\
38
+ msg = "StandardKeyBuilder only supports 40 and 128 bit\
63
39
  encryption (#{@key_length * 8}bit)"
64
- raise ArgumentError, msg
40
+ raise UnsupportedFeatureError, msg
65
41
  end
66
42
  end
67
43
 
68
- # This handler supports all encryption that follows upto PDF 1.5 spec (revision 4)
69
- def self.supports?(encrypt)
70
- return false if encrypt.nil?
71
-
72
- filter = encrypt.fetch(:Filter, :Standard)
73
- version = encrypt.fetch(:V, 0)
74
- algorithm = encrypt.fetch(:CF, {}).fetch(encrypt[:StmF], {}).fetch(:CFM, nil)
75
- (filter == :Standard) && (encrypt[:StmF] == encrypt[:StrF]) &&
76
- (version <= 3 || (version == 4 && ((algorithm == :V2) || (algorithm == :AESV2))))
77
- end
78
-
79
- ##7.6.2 General Encryption Algorithm
80
- #
81
- # Algorithm 1: Encryption of data using the RC4 or AES algorithms
82
- #
83
- # used to decrypt RC4/AES encrypted PDF streams (buf)
44
+ # Takes a string containing a user provided password.
84
45
  #
85
- # buf - a string to decrypt
86
- # 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.
87
49
  #
88
- def decrypt( buf, ref )
89
- case @cfm
90
- when :AESV2
91
- decrypt_aes128(buf, ref)
92
- else
93
- decrypt_rc4(buf, ref)
94
- end
95
- end
96
-
97
- private
50
+ def key(pass)
51
+ pass ||= ""
52
+ encrypt_key = auth_owner_pass(pass)
53
+ encrypt_key ||= auth_user_pass(pass)
98
54
 
99
- # decrypt with RC4 algorithm
100
- # version <=3 or (version == 4 and CFM == V2)
101
- def decrypt_rc4( buf, ref )
102
- objKey = @encrypt_key.dup
103
- (0..2).each { |e| objKey << (ref.id >> e*8 & 0xFF ) }
104
- (0..1).each { |e| objKey << (ref.gen >> e*8 & 0xFF ) }
105
- length = objKey.length < 16 ? objKey.length : 16
106
- rc4 = RC4.new( Digest::MD5.digest(objKey)[0,length] )
107
- rc4.decrypt(buf)
55
+ raise PDF::Reader::EncryptedPDFError, "Invalid password (#{pass})" if encrypt_key.nil?
56
+ encrypt_key
108
57
  end
109
58
 
110
- # decrypt with AES-128-CBC algorithm
111
- # when (version == 4 and CFM == AESV2)
112
- def decrypt_aes128( buf, ref )
113
- objKey = @encrypt_key.dup
114
- (0..2).each { |e| objKey << (ref.id >> e*8 & 0xFF ) }
115
- (0..1).each { |e| objKey << (ref.gen >> e*8 & 0xFF ) }
116
- objKey << 'sAlT' # Algorithm 1, b)
117
- length = objKey.length < 16 ? objKey.length : 16
118
- cipher = OpenSSL::Cipher.new("AES-#{length << 3}-CBC")
119
- cipher.decrypt
120
- cipher.key = Digest::MD5.digest(objKey)[0,length]
121
- cipher.iv = buf[0..15]
122
- cipher.update(buf[16..-1]) + cipher.final
123
- end
59
+ private
124
60
 
125
61
  # Pads supplied password to 32bytes using PassPadBytes as specified on
126
62
  # pp61 of spec
@@ -152,7 +88,7 @@ class PDF::Reader
152
88
  md5 = Digest::MD5.digest(pad_pass(pass))
153
89
  if @revision > 2 then
154
90
  50.times { md5 = Digest::MD5.digest(md5) }
155
- keyBegins = md5[0, key_length]
91
+ keyBegins = md5[0, @key_length]
156
92
  #first iteration decrypt owner_key
157
93
  out = @owner_key
158
94
  #RC4 keyed with (keyBegins XOR with iteration #) to decrypt previous out
@@ -217,12 +153,5 @@ class PDF::Reader
217
153
  end
218
154
  end
219
155
 
220
- def build_standard_key(pass)
221
- encrypt_key = auth_owner_pass(pass)
222
- encrypt_key ||= auth_user_pass(pass)
223
-
224
- raise PDF::Reader::EncryptedPDFError, "Invalid password (#{pass})" if encrypt_key.nil?
225
- encrypt_key
226
- end
227
156
  end
228
157
  end
@@ -1,4 +1,5 @@
1
1
  # coding: utf-8
2
+ # typed: strict
2
3
  # frozen_string_literal: true
3
4
 
4
5
  ################################################################################
@@ -61,7 +62,7 @@ class PDF::Reader
61
62
  end
62
63
 
63
64
  Array(hash[:Filter]).each_with_index do |filter, index|
64
- @udata = Filter.with(filter, options[index]).filter(@udata)
65
+ @udata = Filter.with(filter, options[index] || {}).filter(@udata)
65
66
  end
66
67
  end
67
68
  @udata
@@ -1,4 +1,5 @@
1
1
  # encoding: utf-8
2
+ # typed: true
2
3
  # frozen_string_literal: true
3
4
 
4
5
  # utilities.rb : General-purpose utility classes which don't fit anywhere else
@@ -1,4 +1,5 @@
1
1
  # coding: utf-8
2
+ # typed: true
2
3
  # frozen_string_literal: true
3
4
 
4
5
  class PDF::Reader
@@ -6,15 +7,14 @@ class PDF::Reader
6
7
  class TextRun
7
8
  include Comparable
8
9
 
9
- attr_reader :x, :y, :width, :font_size, :text
10
+ attr_reader :origin, :width, :font_size, :text
10
11
 
11
12
  alias :to_s :text
12
13
 
13
14
  def initialize(x, y, width, font_size, text)
14
- @x = x
15
- @y = y
15
+ @origin = PDF::Reader::Point.new(x, y)
16
16
  @width = width
17
- @font_size = font_size.floor
17
+ @font_size = font_size
18
18
  @text = text
19
19
  end
20
20
 
@@ -34,12 +34,20 @@ class PDF::Reader
34
34
  end
35
35
  end
36
36
 
37
+ def x
38
+ @origin.x
39
+ end
40
+
41
+ def y
42
+ @origin.y
43
+ end
44
+
37
45
  def endx
38
- @endx ||= x + width
46
+ @endx ||= @origin.x + width
39
47
  end
40
48
 
41
49
  def endy
42
- @endy ||= y + font_size
50
+ @endy ||= @origin.y + font_size
43
51
  end
44
52
 
45
53
  def mean_character_width
@@ -1,4 +1,5 @@
1
1
  # coding: utf-8
2
+ # typed: strict
2
3
  # frozen_string_literal: true
3
4
 
4
5
  ################################################################################
@@ -1,4 +1,5 @@
1
1
  # coding: utf-8
2
+ # typed: true
2
3
  # frozen_string_literal: true
3
4
 
4
5
  class PDF::Reader
@@ -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
+
@@ -1,4 +1,5 @@
1
1
  # coding: utf-8
2
+ # typed: strict
2
3
  # frozen_string_literal: true
3
4
 
4
5
  class PDF::Reader
@@ -0,0 +1,262 @@
1
+ # coding: utf-8
2
+ # typed: strict
3
+ # frozen_string_literal: true
4
+
5
+ module PDF
6
+ class Reader
7
+
8
+ # Page#walk will execute the content stream of a page, calling methods on a receiver class
9
+ # provided by the user. Each operator has a specific set of parameters it expects, and we
10
+ # wrap the users receiver class in this one to verify the PDF uses valid parameters.
11
+ #
12
+ # Without these checks, users can't be confident about the number of parameters they'll receive
13
+ # for an operator, or what the type of those parameters will be. Everyone ends up building their
14
+ # own type safety guard clauses and it's tedious.
15
+ #
16
+ # Not all operators have type safety implemented yet, but we can expand the number over time.
17
+ class ValidatingReceiver
18
+
19
+ def initialize(wrapped)
20
+ @wrapped = wrapped
21
+ end
22
+
23
+ def page=(page)
24
+ call_wrapped(:page=, page)
25
+ end
26
+
27
+ #####################################################
28
+ # Graphics State Operators
29
+ #####################################################
30
+ def save_graphics_state(*args)
31
+ call_wrapped(:save_graphics_state)
32
+ end
33
+
34
+ def restore_graphics_state(*args)
35
+ call_wrapped(:restore_graphics_state)
36
+ end
37
+
38
+ #####################################################
39
+ # Matrix Operators
40
+ #####################################################
41
+
42
+ def concatenate_matrix(*args)
43
+ a, b, c, d, e, f = *args
44
+ call_wrapped(
45
+ :concatenate_matrix,
46
+ TypeCheck.cast_to_numeric!(a),
47
+ TypeCheck.cast_to_numeric!(b),
48
+ TypeCheck.cast_to_numeric!(c),
49
+ TypeCheck.cast_to_numeric!(d),
50
+ TypeCheck.cast_to_numeric!(e),
51
+ TypeCheck.cast_to_numeric!(f),
52
+ )
53
+ end
54
+
55
+ #####################################################
56
+ # Text Object Operators
57
+ #####################################################
58
+
59
+ def begin_text_object(*args)
60
+ call_wrapped(:begin_text_object)
61
+ end
62
+
63
+ def end_text_object(*args)
64
+ call_wrapped(:end_text_object)
65
+ end
66
+
67
+ #####################################################
68
+ # Text State Operators
69
+ #####################################################
70
+ def set_character_spacing(*args)
71
+ char_spacing, _ = *args
72
+ call_wrapped(
73
+ :set_character_spacing,
74
+ TypeCheck.cast_to_numeric!(char_spacing)
75
+ )
76
+ end
77
+
78
+ def set_horizontal_text_scaling(*args)
79
+ h_scaling, _ = *args
80
+ call_wrapped(
81
+ :set_horizontal_text_scaling,
82
+ TypeCheck.cast_to_numeric!(h_scaling)
83
+ )
84
+ end
85
+
86
+ def set_text_font_and_size(*args)
87
+ label, size, _ = *args
88
+ call_wrapped(
89
+ :set_text_font_and_size,
90
+ TypeCheck.cast_to_symbol(label),
91
+ TypeCheck.cast_to_numeric!(size)
92
+ )
93
+ end
94
+
95
+ def set_text_leading(*args)
96
+ leading, _ = *args
97
+ call_wrapped(
98
+ :set_text_leading,
99
+ TypeCheck.cast_to_numeric!(leading)
100
+ )
101
+ end
102
+
103
+ def set_text_rendering_mode(*args)
104
+ mode, _ = *args
105
+ call_wrapped(
106
+ :set_text_rendering_mode,
107
+ TypeCheck.cast_to_numeric!(mode)
108
+ )
109
+ end
110
+
111
+ def set_text_rise(*args)
112
+ rise, _ = *args
113
+ call_wrapped(
114
+ :set_text_rise,
115
+ TypeCheck.cast_to_numeric!(rise)
116
+ )
117
+ end
118
+
119
+ def set_word_spacing(*args)
120
+ word_spacing, _ = *args
121
+ call_wrapped(
122
+ :set_word_spacing,
123
+ TypeCheck.cast_to_numeric!(word_spacing)
124
+ )
125
+ end
126
+
127
+ #####################################################
128
+ # Text Positioning Operators
129
+ #####################################################
130
+
131
+ def move_text_position(*args) # Td
132
+ x, y, _ = *args
133
+ call_wrapped(
134
+ :move_text_position,
135
+ TypeCheck.cast_to_numeric!(x),
136
+ TypeCheck.cast_to_numeric!(y)
137
+ )
138
+ end
139
+
140
+ def move_text_position_and_set_leading(*args) # TD
141
+ x, y, _ = *args
142
+ call_wrapped(
143
+ :move_text_position_and_set_leading,
144
+ TypeCheck.cast_to_numeric!(x),
145
+ TypeCheck.cast_to_numeric!(y)
146
+ )
147
+ end
148
+
149
+ def set_text_matrix_and_text_line_matrix(*args) # Tm
150
+ a, b, c, d, e, f = *args
151
+ call_wrapped(
152
+ :set_text_matrix_and_text_line_matrix,
153
+ TypeCheck.cast_to_numeric!(a),
154
+ TypeCheck.cast_to_numeric!(b),
155
+ TypeCheck.cast_to_numeric!(c),
156
+ TypeCheck.cast_to_numeric!(d),
157
+ TypeCheck.cast_to_numeric!(e),
158
+ TypeCheck.cast_to_numeric!(f),
159
+ )
160
+ end
161
+
162
+ def move_to_start_of_next_line(*args) # T*
163
+ call_wrapped(:move_to_start_of_next_line)
164
+ end
165
+
166
+ #####################################################
167
+ # Text Showing Operators
168
+ #####################################################
169
+ def show_text(*args) # Tj (AWAY)
170
+ string, _ = *args
171
+ call_wrapped(
172
+ :show_text,
173
+ TypeCheck.cast_to_string!(string)
174
+ )
175
+ end
176
+
177
+ def show_text_with_positioning(*args) # TJ [(A) 120 (WA) 20 (Y)]
178
+ params, _ = *args
179
+ unless params.is_a?(Array)
180
+ raise MalformedPDFError, "TJ operator expects a single Array argument"
181
+ end
182
+
183
+ call_wrapped(
184
+ :show_text_with_positioning,
185
+ params
186
+ )
187
+ end
188
+
189
+ def move_to_next_line_and_show_text(*args) # '
190
+ string, _ = *args
191
+ call_wrapped(
192
+ :move_to_next_line_and_show_text,
193
+ TypeCheck.cast_to_string!(string)
194
+ )
195
+ end
196
+
197
+ def set_spacing_next_line_show_text(*args) # "
198
+ aw, ac, string = *args
199
+ call_wrapped(
200
+ :set_spacing_next_line_show_text,
201
+ TypeCheck.cast_to_numeric!(aw),
202
+ TypeCheck.cast_to_numeric!(ac),
203
+ TypeCheck.cast_to_string!(string)
204
+ )
205
+ end
206
+
207
+ #####################################################
208
+ # Form XObject Operators
209
+ #####################################################
210
+
211
+ def invoke_xobject(*args)
212
+ label, _ = *args
213
+
214
+ call_wrapped(
215
+ :invoke_xobject,
216
+ TypeCheck.cast_to_symbol(label)
217
+ )
218
+ end
219
+
220
+ #####################################################
221
+ # Inline Image Operators
222
+ #####################################################
223
+
224
+ def begin_inline_image(*args)
225
+ call_wrapped(:begin_inline_image)
226
+ end
227
+
228
+ def begin_inline_image_data(*args)
229
+ # We can't use call_wrapped() here because sorbet won't allow splat args with a dynamic
230
+ # number of elements
231
+ @wrapped.begin_inline_image_data(*args) if @wrapped.respond_to?(:begin_inline_image_data)
232
+ end
233
+
234
+ def end_inline_image(*args)
235
+ data, _ = *args
236
+
237
+ call_wrapped(
238
+ :end_inline_image,
239
+ TypeCheck.cast_to_string!(data)
240
+ )
241
+ end
242
+
243
+ #####################################################
244
+ # Final safety net for any operators that don't have type checking enabled yet
245
+ #####################################################
246
+
247
+ def respond_to?(meth)
248
+ @wrapped.respond_to?(meth)
249
+ end
250
+
251
+ def method_missing(methodname, *args)
252
+ @wrapped.call(methodname, *args)
253
+ end
254
+
255
+ private
256
+
257
+ def call_wrapped(methodname, *args)
258
+ @wrapped.send(methodname, *args) if @wrapped.respond_to?(methodname)
259
+ end
260
+ end
261
+ end
262
+ end
@@ -1,4 +1,5 @@
1
1
  # coding: utf-8
2
+ # typed: true
2
3
  # frozen_string_literal: true
3
4
 
4
5
  require 'afm'
@@ -1,4 +1,5 @@
1
1
  # coding: utf-8
2
+ # typed: true
2
3
  # frozen_string_literal: true
3
4
 
4
5
  class PDF::Reader
@@ -1,4 +1,5 @@
1
1
  # coding: utf-8
2
+ # typed: true
2
3
  # frozen_string_literal: true
3
4
 
4
5
  class PDF::Reader
@@ -29,7 +30,7 @@ class PDF::Reader
29
30
 
30
31
  # in ruby a negative index is valid, and will go from the end of the array
31
32
  # which is undesireable in this case.
32
- if @font.first_char <= code_point
33
+ if @font.first_char && @font.first_char <= code_point
33
34
  @font.widths.fetch(code_point - @font.first_char, @missing_width).to_f
34
35
  else
35
36
  @missing_width.to_f
@@ -1,4 +1,5 @@
1
1
  # coding: utf-8
2
+ # typed: true
2
3
  # frozen_string_literal: true
3
4
 
4
5
  class PDF::Reader
@@ -1,4 +1,5 @@
1
1
  # coding: utf-8
2
+ # typed: true
2
3
  # frozen_string_literal: true
3
4
 
4
5
  class PDF::Reader