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.
@@ -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
- #: (PDF::Reader::Buffer, ?PDF::Reader::ObjectHash?) -> void
66
- def initialize(buffer, objects=nil)
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
- # operators - a hash of supported operators to read from the underlying buffer.
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(operators={})
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 =~ /\d*\.\d/
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
- tok = tok.dup.gsub(/#([A-Fa-f0-9]{2})/) do |match|
173
- res = match[1, 2]
174
- res ? res.hex.chr : ""
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 = "".dup
233
+ str = @buffer.token
203
234
 
204
- loop do
205
- token = @buffer.token
206
- break if token == ">"
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].pack('H*')
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
@@ -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 = RC4.new( Digest::MD5.digest(objKey)[0,length] )
35
+ rc4 = Rc4.new( Digest::MD5.digest(objKey)[0,length] )
36
36
  rc4.decrypt(buf)
37
37
  end
38
38
 
@@ -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 >= bottom_left.x && point.x <= top_right.x &&
76
- point.y >= bottom_left.y && point.y <= top_right.y
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
@@ -71,7 +71,7 @@ class PDF::Reader
71
71
  # return an identical hash
72
72
  #: () -> Integer
73
73
  def hash
74
- "#{self.id}:#{self.gen}".hash
74
+ (id << 16) ^ gen
75
75
  end
76
76
  ################################################################################
77
77
  end
@@ -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 = @owner_key
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
- # The RC4 gem doen't have type annotations, so the type checker doesn't
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 = RC4.new( md5[0, 5] ).decrypt( @owner_key )
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=RC4.new(xor_each_byte(keyBegins, i)).encrypt(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 = RC4.new(keyBegins).encrypt(PassPadBytes.pack("C*")) == @user_key
134
+ pass = Rc4.new(keyBegins).encrypt(PassPadBytes.pack("C*")) == @user_key
136
135
  end
137
136
  pass ? keyBegins : nil
138
137
  end
@@ -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
- @origin = PDF::Reader::Point.new(x, y) #: PDF::Reader::Point
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
- @origin.x
61
+ @x
57
62
  end
58
63
 
59
64
  #: () -> Numeric
60
65
  def y
61
- @origin.y
66
+ @y
62
67
  end
63
68
 
64
69
  #: () -> Numeric
65
70
  def endx
66
- @endx ||= @origin.x + width
71
+ @endx ||= @x + width
67
72
  end
68
73
 
69
74
  #: () -> Numeric
70
75
  def endy
71
- @endy ||= @origin.y + font_size
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
@@ -50,7 +50,7 @@ module PDF
50
50
  end
51
51
  end
52
52
 
53
- #: (untyped) -> Symbol | nil
53
+ #: (untyped) -> (Symbol | nil)
54
54
  def self.cast_to_symbol(obj)
55
55
  if obj.is_a?(Symbol)
56
56
  obj
@@ -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
- # TODO should this fix be logged?
161
- objid = 0 if objid == 1 and offset == 0 and generation == 65535 and state == 'f'
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
- doc_strings_to_utf8(dict)
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'