pdf-reader 2.6.0 → 2.9.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG +30 -1
- data/Rakefile +1 -1
- data/examples/rspec.rb +1 -0
- 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 -33
- data/lib/pdf/reader/cid_widths.rb +1 -0
- data/lib/pdf/reader/cmap.rb +65 -50
- data/lib/pdf/reader/encoding.rb +2 -1
- data/lib/pdf/reader/error.rb +16 -0
- data/lib/pdf/reader/filter/ascii85.rb +2 -0
- data/lib/pdf/reader/filter/ascii_hex.rb +6 -1
- data/lib/pdf/reader/filter/depredict.rb +8 -6
- data/lib/pdf/reader/filter/flate.rb +4 -2
- data/lib/pdf/reader/filter/lzw.rb +2 -0
- data/lib/pdf/reader/filter/null.rb +1 -1
- data/lib/pdf/reader/filter/run_length.rb +19 -13
- data/lib/pdf/reader/filter.rb +11 -11
- data/lib/pdf/reader/font.rb +72 -16
- data/lib/pdf/reader/font_descriptor.rb +19 -17
- data/lib/pdf/reader/form_xobject.rb +15 -5
- data/lib/pdf/reader/glyph_hash.rb +1 -0
- data/lib/pdf/reader/key_builder_v5.rb +138 -0
- data/lib/pdf/reader/lzw.rb +4 -2
- data/lib/pdf/reader/null_security_handler.rb +1 -4
- data/lib/pdf/reader/object_cache.rb +1 -0
- data/lib/pdf/reader/object_hash.rb +252 -44
- data/lib/pdf/reader/object_stream.rb +1 -0
- data/lib/pdf/reader/overlapping_runs_filter.rb +11 -4
- data/lib/pdf/reader/page.rb +99 -19
- data/lib/pdf/reader/page_layout.rb +28 -32
- data/lib/pdf/reader/page_state.rb +12 -11
- data/lib/pdf/reader/page_text_receiver.rb +57 -10
- data/lib/pdf/reader/pages_strategy.rb +1 -0
- data/lib/pdf/reader/parser.rb +26 -8
- data/lib/pdf/reader/point.rb +25 -0
- data/lib/pdf/reader/print_receiver.rb +1 -0
- data/lib/pdf/reader/rc4_security_handler.rb +38 -0
- data/lib/pdf/reader/rectangle.rb +113 -0
- data/lib/pdf/reader/reference.rb +1 -0
- data/lib/pdf/reader/register_receiver.rb +1 -0
- data/lib/pdf/reader/{resource_methods.rb → resources.rb} +16 -9
- data/lib/pdf/reader/security_handler_factory.rb +79 -0
- data/lib/pdf/reader/{standard_security_handler.rb → standard_key_builder.rb} +23 -94
- data/lib/pdf/reader/stream.rb +2 -1
- data/lib/pdf/reader/synchronized_cache.rb +1 -0
- data/lib/pdf/reader/text_run.rb +14 -6
- data/lib/pdf/reader/token.rb +1 -0
- data/lib/pdf/reader/transformation_matrix.rb +1 -0
- data/lib/pdf/reader/type_check.rb +52 -0
- data/lib/pdf/reader/unimplemented_security_handler.rb +1 -0
- data/lib/pdf/reader/validating_receiver.rb +262 -0
- data/lib/pdf/reader/width_calculator/built_in.rb +1 -0
- data/lib/pdf/reader/width_calculator/composite.rb +1 -0
- data/lib/pdf/reader/width_calculator/true_type.rb +2 -1
- data/lib/pdf/reader/width_calculator/type_one_or_three.rb +1 -0
- data/lib/pdf/reader/width_calculator/type_zero.rb +1 -0
- data/lib/pdf/reader/width_calculator.rb +1 -0
- data/lib/pdf/reader/xref.rb +21 -3
- data/lib/pdf/reader/zero_width_runs_filter.rb +2 -0
- data/lib/pdf/reader.rb +46 -15
- data/lib/pdf-reader.rb +1 -0
- data/rbi/pdf-reader.rbi +1978 -0
- metadata +22 -13
- data/lib/pdf/reader/orientation_detector.rb +0 -34
- 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
|
-
#
|
35
|
-
|
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 = "
|
38
|
+
msg = "StandardKeyBuilder only supports 40 and 128 bit\
|
63
39
|
encryption (#{@key_length * 8}bit)"
|
64
|
-
raise
|
40
|
+
raise UnsupportedFeatureError, msg
|
65
41
|
end
|
66
42
|
end
|
67
43
|
|
68
|
-
#
|
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
|
-
#
|
86
|
-
#
|
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
|
89
|
-
|
90
|
-
|
91
|
-
|
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
|
-
|
100
|
-
|
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
|
-
|
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
|
data/lib/pdf/reader/stream.rb
CHANGED
@@ -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
|
data/lib/pdf/reader/text_run.rb
CHANGED
@@ -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 :
|
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
|
-
@
|
15
|
-
@y = y
|
15
|
+
@origin = PDF::Reader::Point.new(x, y)
|
16
16
|
@width = width
|
17
|
-
@font_size = font_size
|
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
|
data/lib/pdf/reader/token.rb
CHANGED
@@ -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
|
+
|
@@ -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.send(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
|
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
|