pdf-reader 2.15.0 → 2.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG +29 -2
- data/Rakefile +1 -1
- data/lib/pdf/reader/bounding_rectangle_runs_filter.rb +1 -1
- data/lib/pdf/reader/buffer.rb +82 -30
- data/lib/pdf/reader/cmap.rb +14 -7
- data/lib/pdf/reader/encoding.rb +42 -26
- data/lib/pdf/reader/encoding_utils.rb +128 -0
- data/lib/pdf/reader/error.rb +16 -6
- data/lib/pdf/reader/font.rb +40 -21
- data/lib/pdf/reader/form_xobject.rb +5 -2
- data/lib/pdf/reader/key_builder_v5.rb +0 -1
- data/lib/pdf/reader/object_cache.rb +11 -7
- data/lib/pdf/reader/object_hash.rb +14 -9
- data/lib/pdf/reader/page.rb +2 -2
- data/lib/pdf/reader/page_layout.rb +1 -1
- data/lib/pdf/reader/page_state.rb +73 -29
- data/lib/pdf/reader/page_text_receiver.rb +43 -13
- data/lib/pdf/reader/pages_strategy.rb +1 -1
- data/lib/pdf/reader/parser.rb +52 -18
- data/lib/pdf/reader/point.rb +6 -0
- data/lib/pdf/reader/rc4.rb +43 -0
- data/lib/pdf/reader/rc4_security_handler.rb +2 -2
- data/lib/pdf/reader/rectangle.rb +13 -2
- data/lib/pdf/reader/reference.rb +1 -1
- data/lib/pdf/reader/standard_key_builder.rb +9 -10
- data/lib/pdf/reader/text_run.rb +13 -8
- data/lib/pdf/reader/transformation_matrix.rb +11 -0
- data/lib/pdf/reader/type_check.rb +1 -1
- data/lib/pdf/reader/xref.rb +6 -3
- data/lib/pdf/reader.rb +3 -53
- data/rbi/pdf-reader.rbi +129 -36
- metadata +6 -18
data/lib/pdf/reader/parser.rb
CHANGED
|
@@ -34,6 +34,7 @@ class PDF::Reader
|
|
|
34
34
|
class Parser
|
|
35
35
|
|
|
36
36
|
TOKEN_STRATEGY = proc { |parser, token| Token.new(token) } #: Proc
|
|
37
|
+
INTERNED_TOKENS = {} #: Hash[String, PDF::Reader::Token]
|
|
37
38
|
|
|
38
39
|
STRATEGIES = {
|
|
39
40
|
"/" => proc { |parser, token| parser.send(:pdf_name) },
|
|
@@ -62,17 +63,28 @@ class PDF::Reader
|
|
|
62
63
|
#
|
|
63
64
|
# buffer - a PDF::Reader::Buffer object that contains PDF data
|
|
64
65
|
# objects - a PDF::Reader::ObjectHash object that can return objects from the PDF file
|
|
65
|
-
|
|
66
|
-
|
|
66
|
+
# operators - a hash of supported operators to read from the underlying buffer.
|
|
67
|
+
# relaxed_dictionaries - quietly skip unexpected operator tokens inside a dictionary. Useful for
|
|
68
|
+
# handling Postscript dictionaries in CMaps
|
|
69
|
+
#
|
|
70
|
+
#: (
|
|
71
|
+
#| PDF::Reader::Buffer,
|
|
72
|
+
#| ?operators: Hash[String | PDF::Reader::Token, Symbol],
|
|
73
|
+
#| ?objects: PDF::Reader::ObjectHash?,
|
|
74
|
+
#| ?relaxed_dictionaries: T::Boolean
|
|
75
|
+
#| ) -> void
|
|
76
|
+
def initialize(buffer, operators: {}, objects: nil, relaxed_dictionaries: false)
|
|
67
77
|
@buffer = buffer
|
|
78
|
+
@operators = operators
|
|
68
79
|
@objects = objects
|
|
80
|
+
@relaxed_dictionaries = relaxed_dictionaries
|
|
81
|
+
@hex_pack_buffer = [""] #: Array[String]
|
|
69
82
|
end
|
|
70
83
|
################################################################################
|
|
71
84
|
# Reads the next token from the underlying buffer and convets it to an appropriate
|
|
72
85
|
# object
|
|
73
86
|
#
|
|
74
|
-
|
|
75
|
-
#: (?Hash[String | PDF::Reader::Token, Symbol]) -> (
|
|
87
|
+
#: () -> (
|
|
76
88
|
#| PDF::Reader::Reference |
|
|
77
89
|
#| PDF::Reader::Token |
|
|
78
90
|
#| Numeric |
|
|
@@ -82,7 +94,7 @@ class PDF::Reader
|
|
|
82
94
|
#| Hash[untyped, untyped] |
|
|
83
95
|
#| nil
|
|
84
96
|
#| )
|
|
85
|
-
def parse_token
|
|
97
|
+
def parse_token
|
|
86
98
|
token = @buffer.token
|
|
87
99
|
|
|
88
100
|
if token.nil?
|
|
@@ -92,11 +104,11 @@ class PDF::Reader
|
|
|
92
104
|
proc.call(self, token) if proc
|
|
93
105
|
elsif token.is_a? PDF::Reader::Reference
|
|
94
106
|
token
|
|
95
|
-
elsif operators.has_key? token
|
|
96
|
-
Token.new(token)
|
|
107
|
+
elsif @operators.has_key? token
|
|
108
|
+
INTERNED_TOKENS[token] ||= Token.new(token)
|
|
97
109
|
elsif token.frozen?
|
|
98
110
|
token
|
|
99
|
-
elsif token
|
|
111
|
+
elsif match?(token, /\d*\.\d/)
|
|
100
112
|
token.to_f
|
|
101
113
|
else
|
|
102
114
|
token.to_i
|
|
@@ -143,6 +155,18 @@ class PDF::Reader
|
|
|
143
155
|
|
|
144
156
|
private
|
|
145
157
|
|
|
158
|
+
# Once min ruby version is >= 2.4, we can drop this method and just use String#match?
|
|
159
|
+
#
|
|
160
|
+
#: (String, Regexp) -> bool
|
|
161
|
+
def match?(str, regexp)
|
|
162
|
+
# We prefer match? because fewer objects are allocated, and this code path is hot
|
|
163
|
+
if str.respond_to?(:match?)
|
|
164
|
+
str.match?(regexp)
|
|
165
|
+
else
|
|
166
|
+
str.match(regexp) != nil
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
146
170
|
################################################################################
|
|
147
171
|
# reads a PDF dict from the buffer and converts it to a Ruby Hash.
|
|
148
172
|
#: () -> Hash[Symbol, untyped]
|
|
@@ -152,6 +176,11 @@ class PDF::Reader
|
|
|
152
176
|
loop do
|
|
153
177
|
key = parse_token
|
|
154
178
|
break if key.kind_of?(Token) and key == ">>"
|
|
179
|
+
|
|
180
|
+
# Skip operator tokens (e.g. PostScript "def") that appear inside
|
|
181
|
+
# dictionary blocks in CMap streams parsed with operator keywords.
|
|
182
|
+
next if @relaxed_dictionaries && key.kind_of?(Token)
|
|
183
|
+
|
|
155
184
|
raise MalformedPDFError, "unterminated dict" if @buffer.empty?
|
|
156
185
|
PDF::Reader::Error.validate_type_as_malformed(key, "Dictionary key", Symbol)
|
|
157
186
|
|
|
@@ -169,9 +198,11 @@ class PDF::Reader
|
|
|
169
198
|
tok = @buffer.token
|
|
170
199
|
|
|
171
200
|
if tok.is_a?(String)
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
201
|
+
if tok.include?('#')
|
|
202
|
+
tok = tok.gsub(/#([A-Fa-f0-9]{2})/) do |match|
|
|
203
|
+
res = match[1, 2]
|
|
204
|
+
res ? res.hex.chr : ""
|
|
205
|
+
end
|
|
175
206
|
end
|
|
176
207
|
tok.to_sym
|
|
177
208
|
elsif tok.is_a?(PDF::Reader::Reference)
|
|
@@ -199,18 +230,21 @@ class PDF::Reader
|
|
|
199
230
|
# Reads a PDF hex string from the buffer and converts it to a Ruby String
|
|
200
231
|
#: () -> String
|
|
201
232
|
def hex_string
|
|
202
|
-
str =
|
|
233
|
+
str = @buffer.token
|
|
203
234
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
raise MalformedPDFError, "unterminated hex string" if @buffer.empty?
|
|
208
|
-
str << token
|
|
235
|
+
if str == ">"
|
|
236
|
+
# empty hex string
|
|
237
|
+
return "".dup.force_encoding("binary")
|
|
209
238
|
end
|
|
210
239
|
|
|
240
|
+
raise MalformedPDFError, "unterminated hex string" if str.nil? || @buffer.empty?
|
|
241
|
+
raise MalformedPDFError, "invalid hex string" unless str.is_a?(String)
|
|
242
|
+
@buffer.token # consume the closing ">"
|
|
243
|
+
|
|
211
244
|
# add a missing digit if required, as required by the spec
|
|
212
245
|
str << "0" unless str.size % 2 == 0
|
|
213
|
-
[str
|
|
246
|
+
@hex_pack_buffer[0] = str
|
|
247
|
+
@hex_pack_buffer.pack('H*')
|
|
214
248
|
end
|
|
215
249
|
################################################################################
|
|
216
250
|
# Reads a PDF String from the buffer and converts it to a Ruby String
|
data/lib/pdf/reader/point.rb
CHANGED
|
@@ -27,6 +27,12 @@ module PDF
|
|
|
27
27
|
other.respond_to?(:x) && other.respond_to?(:y) && x == other.x && y == other.y
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
+
# These two points are super common, so make them available as constants to reduce
|
|
31
|
+
# object allocations. Points are immutable, so it's fine to have multiple code paths
|
|
32
|
+
# using the same object
|
|
33
|
+
ZERO_ZERO = self.new(0, 0) #: PDF::Reader::Point
|
|
34
|
+
ONE_ONE = self.new(1, 1) #: PDF::Reader::Point
|
|
35
|
+
|
|
30
36
|
end
|
|
31
37
|
end
|
|
32
38
|
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
# typed: strict
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
class PDF::Reader
|
|
6
|
+
# A minimal, self-contained implementation of the RC4 stream cipher, used to decrypt
|
|
7
|
+
# PDFs encrypted with the (legacy, but still spec-compliant) RC4 security handler.
|
|
8
|
+
# Vendored directly to avoid depending on the abandoned ruby-rc4 gem (last released
|
|
9
|
+
# 2012, archived since 2020, and missing license metadata in its gemspec).
|
|
10
|
+
class Rc4 # :nodoc:
|
|
11
|
+
#: (String) -> void
|
|
12
|
+
def initialize(key)
|
|
13
|
+
raise ArgumentError, "key must not be empty" if key.empty?
|
|
14
|
+
|
|
15
|
+
@key = key.bytes #: Array[Integer]
|
|
16
|
+
@s = (0..255).to_a #: Array[Integer]
|
|
17
|
+
j = 0
|
|
18
|
+
256.times do |i|
|
|
19
|
+
j = (j + @s.fetch(i) + @key.fetch(i % @key.length)) & 0xFF
|
|
20
|
+
@s[i], @s[j] = @s.fetch(j), @s.fetch(i)
|
|
21
|
+
end
|
|
22
|
+
@i = 0 #: Integer
|
|
23
|
+
@j = 0 #: Integer
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# RC4 encryption and decryption are the same operation (XOR with the keystream).
|
|
27
|
+
#: (String) -> String
|
|
28
|
+
def decrypt(data)
|
|
29
|
+
out = "".b
|
|
30
|
+
data = data.dup.force_encoding(::Encoding::ASCII_8BIT)
|
|
31
|
+
data.each_byte do |byte|
|
|
32
|
+
@i = (@i + 1) & 0xFF
|
|
33
|
+
@j = (@j + @s.fetch(@i)) & 0xFF
|
|
34
|
+
@s[@i], @s[@j] = @s.fetch(@j), @s.fetch(@i)
|
|
35
|
+
k = @s.fetch((@s.fetch(@i) + @s.fetch(@j)) & 0xFF)
|
|
36
|
+
out << (byte ^ k)
|
|
37
|
+
end
|
|
38
|
+
out
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
alias_method :encrypt, :decrypt
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# frozen_string_literal: true
|
|
4
4
|
|
|
5
5
|
require 'digest/md5'
|
|
6
|
-
require 'rc4'
|
|
6
|
+
require 'pdf/reader/rc4'
|
|
7
7
|
|
|
8
8
|
class PDF::Reader
|
|
9
9
|
|
|
@@ -32,7 +32,7 @@ class PDF::Reader
|
|
|
32
32
|
(0..2).each { |e| objKey << (ref.id >> e*8 & 0xFF ) }
|
|
33
33
|
(0..1).each { |e| objKey << (ref.gen >> e*8 & 0xFF ) }
|
|
34
34
|
length = objKey.length < 16 ? objKey.length : 16
|
|
35
|
-
rc4 =
|
|
35
|
+
rc4 = Rc4.new( Digest::MD5.digest(objKey)[0,length] )
|
|
36
36
|
rc4.decrypt(buf)
|
|
37
37
|
end
|
|
38
38
|
|
data/lib/pdf/reader/rectangle.rb
CHANGED
|
@@ -70,10 +70,21 @@ module PDF
|
|
|
70
70
|
bottom_right.x - bottom_left.x
|
|
71
71
|
end
|
|
72
72
|
|
|
73
|
+
# Is the point inside this rectangle? Nicer to read and use than contains_xy?(), but
|
|
74
|
+
# allocates more objects so be careful using on hot code paths.
|
|
75
|
+
#
|
|
73
76
|
#: (PDF::Reader::Point) -> bool
|
|
74
77
|
def contains?(point)
|
|
75
|
-
point.x
|
|
76
|
-
|
|
78
|
+
contains_xy?(point.x, point.y)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Is the point inside this rectangle? Worse to read and use than contains?(), but
|
|
82
|
+
# allocates fewer objects so may be preferrable on hot code paths.
|
|
83
|
+
#
|
|
84
|
+
#: (Numeric, Numeric) -> bool
|
|
85
|
+
def contains_xy?(x, y)
|
|
86
|
+
x >= bottom_left.x && x <= top_right.x &&
|
|
87
|
+
y >= bottom_left.y && y <= top_right.y
|
|
77
88
|
end
|
|
78
89
|
|
|
79
90
|
# A pdf-style 4-number array
|
data/lib/pdf/reader/reference.rb
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# typed: strict
|
|
3
3
|
|
|
4
4
|
require 'digest/md5'
|
|
5
|
-
require 'rc4'
|
|
5
|
+
require 'pdf/reader/rc4'
|
|
6
6
|
|
|
7
7
|
class PDF::Reader
|
|
8
8
|
|
|
@@ -91,22 +91,21 @@ class PDF::Reader
|
|
|
91
91
|
#
|
|
92
92
|
#: (String) -> String?
|
|
93
93
|
def auth_owner_pass(pass)
|
|
94
|
+
owner_key = @owner_key
|
|
95
|
+
raise MalformedPDFError, "encrypted PDF is missing an owner key" if owner_key.nil?
|
|
96
|
+
|
|
94
97
|
md5 = Digest::MD5.digest(pad_pass(pass))
|
|
95
98
|
if @revision > 2 then
|
|
96
99
|
50.times { md5 = Digest::MD5.digest(md5) }
|
|
97
100
|
keyBegins = md5[0, @key_length]
|
|
98
101
|
#first iteration decrypt owner_key
|
|
99
|
-
out =
|
|
102
|
+
out = owner_key
|
|
100
103
|
#RC4 keyed with (keyBegins XOR with iteration #) to decrypt previous out
|
|
101
104
|
19.downto(0).each { |i|
|
|
102
|
-
|
|
103
|
-
# know decrypt() returns a string
|
|
104
|
-
out = TypeCheck.cast_to_string!(
|
|
105
|
-
RC4.new(xor_each_byte(keyBegins,i)).decrypt(out)
|
|
106
|
-
)
|
|
105
|
+
out = Rc4.new(xor_each_byte(keyBegins,i)).decrypt(out)
|
|
107
106
|
}
|
|
108
107
|
else
|
|
109
|
-
out =
|
|
108
|
+
out = Rc4.new( md5[0, 5] ).decrypt( owner_key )
|
|
110
109
|
end
|
|
111
110
|
# c) check output as user password
|
|
112
111
|
auth_user_pass( out )
|
|
@@ -129,10 +128,10 @@ class PDF::Reader
|
|
|
129
128
|
#initialize out for first iteration
|
|
130
129
|
out = Digest::MD5.digest(PassPadBytes.pack("C*") + @file_id)
|
|
131
130
|
#zero doesn't matter -> so from 0-19
|
|
132
|
-
20.times{ |i| out=
|
|
131
|
+
20.times{ |i| out=Rc4.new(xor_each_byte(keyBegins, i)).encrypt(out) }
|
|
133
132
|
pass = @user_key.to_s[0, 16] == out
|
|
134
133
|
else
|
|
135
|
-
pass =
|
|
134
|
+
pass = Rc4.new(keyBegins).encrypt(PassPadBytes.pack("C*")) == @user_key
|
|
136
135
|
end
|
|
137
136
|
pass ? keyBegins : nil
|
|
138
137
|
end
|
data/lib/pdf/reader/text_run.rb
CHANGED
|
@@ -7,9 +7,6 @@ class PDF::Reader
|
|
|
7
7
|
class TextRun
|
|
8
8
|
include Comparable
|
|
9
9
|
|
|
10
|
-
#: PDF::Reader::Point
|
|
11
|
-
attr_reader :origin
|
|
12
|
-
|
|
13
10
|
#: Numeric
|
|
14
11
|
attr_reader :width
|
|
15
12
|
|
|
@@ -23,15 +20,23 @@ class PDF::Reader
|
|
|
23
20
|
|
|
24
21
|
#: (Numeric, Numeric, Numeric, Numeric, String) -> void
|
|
25
22
|
def initialize(x, y, width, font_size, text)
|
|
26
|
-
@
|
|
23
|
+
@x = x #: Numeric
|
|
24
|
+
@y = y #: Numeric
|
|
27
25
|
@width = width
|
|
28
26
|
@font_size = font_size
|
|
29
27
|
@text = text
|
|
28
|
+
@origin = nil #: PDF::Reader::Point | nil
|
|
30
29
|
@endx = nil #: Numeric | nil
|
|
31
30
|
@endy = nil #: Numeric | nil
|
|
32
31
|
@mergable_range = nil #: Range[Numeric] | nil
|
|
33
32
|
end
|
|
34
33
|
|
|
34
|
+
# Lazily constructed to avoid allocating a Point on every TextRun
|
|
35
|
+
#: () -> PDF::Reader::Point
|
|
36
|
+
def origin
|
|
37
|
+
@origin ||= PDF::Reader::Point.new(@x, @y)
|
|
38
|
+
end
|
|
39
|
+
|
|
35
40
|
# Allows collections of TextRun objects to be sorted. They will be sorted
|
|
36
41
|
# in order of their position on a cartesian plain - Top Left to Bottom Right
|
|
37
42
|
#: (PDF::Reader::Point) -> Numeric
|
|
@@ -53,22 +58,22 @@ class PDF::Reader
|
|
|
53
58
|
|
|
54
59
|
#: () -> Numeric
|
|
55
60
|
def x
|
|
56
|
-
@
|
|
61
|
+
@x
|
|
57
62
|
end
|
|
58
63
|
|
|
59
64
|
#: () -> Numeric
|
|
60
65
|
def y
|
|
61
|
-
@
|
|
66
|
+
@y
|
|
62
67
|
end
|
|
63
68
|
|
|
64
69
|
#: () -> Numeric
|
|
65
70
|
def endx
|
|
66
|
-
@endx ||= @
|
|
71
|
+
@endx ||= @x + width
|
|
67
72
|
end
|
|
68
73
|
|
|
69
74
|
#: () -> Numeric
|
|
70
75
|
def endy
|
|
71
|
-
@endy ||= @
|
|
76
|
+
@endy ||= @y + font_size
|
|
72
77
|
end
|
|
73
78
|
|
|
74
79
|
#: () -> Numeric
|
|
@@ -54,6 +54,17 @@ class PDF::Reader
|
|
|
54
54
|
@e,@f,1]
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
+
# Apply a left-multiplication by a translation matrix [1, 0, 0, 1, tx, ty]
|
|
58
|
+
# without allocating a new TransformationMatrix.
|
|
59
|
+
#
|
|
60
|
+
#: (Numeric, Numeric) -> void
|
|
61
|
+
def prepend_translation!(tx, ty)
|
|
62
|
+
new_e = (tx * @a) + (ty * @c) + @e
|
|
63
|
+
new_f = (tx * @b) + (ty * @d) + @f
|
|
64
|
+
@e = new_e
|
|
65
|
+
@f = new_f
|
|
66
|
+
end
|
|
67
|
+
|
|
57
68
|
# multiply this matrix with another.
|
|
58
69
|
#
|
|
59
70
|
# the second matrix is represented by the 6 scalar values that are changeable
|
data/lib/pdf/reader/xref.rb
CHANGED
|
@@ -151,14 +151,17 @@ class PDF::Reader
|
|
|
151
151
|
end
|
|
152
152
|
|
|
153
153
|
objid, count = params[0].to_i, params[1].to_i
|
|
154
|
-
count.times do
|
|
154
|
+
count.times do |entry_index|
|
|
155
155
|
offset = buf.token.to_i
|
|
156
156
|
generation = buf.token.to_i
|
|
157
157
|
state = buf.token
|
|
158
158
|
|
|
159
159
|
# Some PDF writers start numbering at 1 instead of 0. Fix up the number.
|
|
160
|
-
#
|
|
161
|
-
|
|
160
|
+
# Only apply this on the first entry of the section - if entry_index > 0,
|
|
161
|
+
# objid has been incremented from 0 and object 1 is a legitimate free entry.
|
|
162
|
+
if entry_index == 0 and objid == 1 and offset == 0 and generation == 65535 and state == 'f'
|
|
163
|
+
objid = 0
|
|
164
|
+
end
|
|
162
165
|
store(objid, generation, offset + @junk_offset) if state == "n" && offset > 0
|
|
163
166
|
objid += 1
|
|
164
167
|
params.clear
|
data/lib/pdf/reader.rb
CHANGED
|
@@ -130,7 +130,7 @@ module PDF
|
|
|
130
130
|
#: () -> Hash[untyped, untyped]?
|
|
131
131
|
def info
|
|
132
132
|
dict = @objects.deref_hash(@objects.trailer[:Info]) || {}
|
|
133
|
-
|
|
133
|
+
EncodingUtils.obj_to_utf8(dict)
|
|
134
134
|
end
|
|
135
135
|
|
|
136
136
|
# Return a String with extra XML metadata provided by the author of the PDF file. Not
|
|
@@ -234,58 +234,6 @@ module PDF
|
|
|
234
234
|
|
|
235
235
|
private
|
|
236
236
|
|
|
237
|
-
# recursively convert strings from outside a content stream into UTF-8
|
|
238
|
-
#
|
|
239
|
-
#: (untyped) -> untyped
|
|
240
|
-
def doc_strings_to_utf8(obj)
|
|
241
|
-
case obj
|
|
242
|
-
when ::Hash then
|
|
243
|
-
{}.tap { |new_hash|
|
|
244
|
-
obj.each do |key, value|
|
|
245
|
-
new_hash[key] = doc_strings_to_utf8(value)
|
|
246
|
-
end
|
|
247
|
-
}
|
|
248
|
-
when Array then
|
|
249
|
-
obj.map { |item| doc_strings_to_utf8(item) }
|
|
250
|
-
when String then
|
|
251
|
-
if has_utf16_bom?(obj)
|
|
252
|
-
utf16_to_utf8(obj)
|
|
253
|
-
else
|
|
254
|
-
pdfdoc_to_utf8(obj)
|
|
255
|
-
end
|
|
256
|
-
else
|
|
257
|
-
obj
|
|
258
|
-
end
|
|
259
|
-
end
|
|
260
|
-
|
|
261
|
-
#: (String) -> bool
|
|
262
|
-
def has_utf16_bom?(str)
|
|
263
|
-
first_bytes = str[0,2]
|
|
264
|
-
|
|
265
|
-
return false if first_bytes.nil?
|
|
266
|
-
|
|
267
|
-
first_bytes.unpack("C*") == [254, 255]
|
|
268
|
-
end
|
|
269
|
-
|
|
270
|
-
# TODO find a PDF I can use to spec this behaviour
|
|
271
|
-
#
|
|
272
|
-
#: (String) -> String
|
|
273
|
-
def pdfdoc_to_utf8(obj)
|
|
274
|
-
obj.force_encoding("utf-8")
|
|
275
|
-
obj
|
|
276
|
-
end
|
|
277
|
-
|
|
278
|
-
# one day we'll all run on a 1.9 compatible VM and I can just do this with
|
|
279
|
-
# String#encode
|
|
280
|
-
#
|
|
281
|
-
#: (String) -> String
|
|
282
|
-
def utf16_to_utf8(obj)
|
|
283
|
-
str = obj[2, obj.size].to_s
|
|
284
|
-
str = str.unpack("n*").pack("U*")
|
|
285
|
-
str.force_encoding("utf-8")
|
|
286
|
-
str
|
|
287
|
-
end
|
|
288
|
-
|
|
289
237
|
#: () -> Hash[Symbol, untyped]
|
|
290
238
|
def root
|
|
291
239
|
@root ||= @objects.deref_hash(@objects.trailer[:Root]) || {}
|
|
@@ -302,6 +250,7 @@ require 'pdf/reader/bounding_rectangle_runs_filter'
|
|
|
302
250
|
require 'pdf/reader/cid_widths'
|
|
303
251
|
require 'pdf/reader/cmap'
|
|
304
252
|
require 'pdf/reader/encoding'
|
|
253
|
+
require 'pdf/reader/encoding_utils'
|
|
305
254
|
require 'pdf/reader/error'
|
|
306
255
|
require 'pdf/reader/filter'
|
|
307
256
|
require 'pdf/reader/filter/ascii85'
|
|
@@ -333,6 +282,7 @@ require 'pdf/reader/standard_key_builder'
|
|
|
333
282
|
require 'pdf/reader/key_builder_v5'
|
|
334
283
|
require 'pdf/reader/aes_v2_security_handler'
|
|
335
284
|
require 'pdf/reader/aes_v3_security_handler'
|
|
285
|
+
require 'pdf/reader/rc4'
|
|
336
286
|
require 'pdf/reader/rc4_security_handler'
|
|
337
287
|
require 'pdf/reader/unimplemented_security_handler'
|
|
338
288
|
require 'pdf/reader/stream'
|