pdf-reader 2.15.1 → 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.
@@ -103,9 +103,12 @@ module PDF
103
103
  def tokens
104
104
  @cache[cached_tokens_key] ||= begin
105
105
  buffer = Buffer.new(StringIO.new(raw_content), :content_stream => true)
106
- parser = Parser.new(buffer, @objects)
106
+ parser = Parser.new(
107
+ buffer,
108
+ operators: PagesStrategy::OPERATORS, objects: @objects
109
+ )
107
110
  result = []
108
- while (token = parser.parse_token(PagesStrategy::OPERATORS))
111
+ while (token = parser.parse_token)
109
112
  result << token
110
113
  end
111
114
  result
@@ -3,7 +3,6 @@
3
3
  # frozen_string_literal: true
4
4
 
5
5
  require 'digest/md5'
6
- require 'rc4'
7
6
 
8
7
  class PDF::Reader
9
8
 
@@ -17,18 +17,18 @@ class PDF::Reader
17
17
  # avoid lots of repetitive (and expensive) tokenising
18
18
  CACHEABLE_TYPES = [:Catalog, :Page, :Pages] #: Array[Symbol]
19
19
 
20
- #: untyped
20
+ #: Integer
21
21
  attr_reader :hits
22
22
 
23
- #: untyped
23
+ #: Integer
24
24
  attr_reader :misses
25
25
 
26
26
  #: (?untyped) -> void
27
27
  def initialize(lru_size = 1000)
28
- @objects = {}
29
- @lru_cache = Hashery::LRUHash.new(lru_size.to_i)
30
- @hits = 0
31
- @misses = 0
28
+ @objects = {} #: Hash[untyped, untyped]
29
+ @lru_cache = Hashery::LRUHash.new(lru_size.to_i) #: Hashery::LRUHash
30
+ @hits = 0 #: Integer
31
+ @misses = 0 #: Integer
32
32
  end
33
33
 
34
34
  def [](key)
@@ -46,7 +46,7 @@ class PDF::Reader
46
46
 
47
47
  def fetch(key, local_default = nil)
48
48
  update_stats(key)
49
- @objects[key] || @lru_cache.fetch(key, local_default)
49
+ @objects[key] || @lru_cache.fetch(key) { local_default }
50
50
  end
51
51
 
52
52
  def each(&block)
@@ -85,14 +85,17 @@ class PDF::Reader
85
85
  @objects.has_value?(value) || @lru_cache.has_value?(value)
86
86
  end
87
87
 
88
+ #: () -> String
88
89
  def to_s
89
90
  "<PDF::Reader::ObjectCache size: #{self.size}>"
90
91
  end
91
92
 
93
+ #: () -> Array[untyped]
92
94
  def keys
93
95
  @objects.keys + @lru_cache.keys
94
96
  end
95
97
 
98
+ #: () -> Array[untyped]
96
99
  def values
97
100
  @objects.values + @lru_cache.values
98
101
  end
@@ -107,6 +110,7 @@ class PDF::Reader
107
110
  end
108
111
  end
109
112
 
113
+ #: (untyped) -> bool?
110
114
  def cacheable?(obj)
111
115
  obj.is_a?(Hash) && CACHEABLE_TYPES.include?(obj[:Type])
112
116
  end
@@ -59,9 +59,9 @@ class PDF::Reader
59
59
  #
60
60
  # :password - the user password to decrypt the source PDF
61
61
  #
62
- #: ((IO | Tempfile | StringIO | String), ?Hash[Symbol, untyped]) -> void
62
+ #: (untyped, ?Hash[Symbol, untyped]) -> void
63
63
  def initialize(input, opts = {})
64
- @io = extract_io_from(input) #: IO | Tempfile | StringIO
64
+ @io = extract_io_from(input) #: untyped
65
65
  @xref = PDF::Reader::XRef.new(@io) #: PDF::Reader::XRef[PDF::Reader::Reference]
66
66
  @pdf_version = read_version #: Float
67
67
  @trailer = @xref.trailer #: Hash[Symbol, untyped]
@@ -127,6 +127,7 @@ class PDF::Reader
127
127
  # Guaranteed to only return an Array or nil. If the dereference results in
128
128
  # any other type then a MalformedPDFError exception will raise. Useful when
129
129
  # expecting an Array and no other type will do.
130
+ #
130
131
  #: (untyped) -> Array[untyped]?
131
132
  def deref_array(key)
132
133
  obj = deref(key)
@@ -146,6 +147,7 @@ class PDF::Reader
146
147
  # expecting an Array and no other type will do.
147
148
  #
148
149
  # Some effort to cast array elements to a number is made for any non-numeric elements.
150
+ #
149
151
  #: (untyped) -> Array[Numeric]?
150
152
  def deref_array_of_numbers(key)
151
153
  arr = deref(key)
@@ -172,7 +174,8 @@ class PDF::Reader
172
174
  #
173
175
  # Guaranteed to only return a Hash or nil. If the dereference results in
174
176
  # any other type then a MalformedPDFError exception will raise. Useful when
175
- # expecting an Array and no other type will do.
177
+ # expecting a Hash and no other type will do.
178
+ #
176
179
  #: (untyped) -> Hash[Symbol, untyped]?
177
180
  def deref_hash(key)
178
181
  obj = deref(key)
@@ -189,9 +192,10 @@ class PDF::Reader
189
192
  #
190
193
  # Guaranteed to only return a PDF name (Symbol) or nil. If the dereference results in
191
194
  # any other type then a MalformedPDFError exception will raise. Useful when
192
- # expecting an Array and no other type will do.
195
+ # expecting a PDF Name and no other type will do.
193
196
  #
194
197
  # Some effort to cast to a symbol is made when the reference points to a non-symbol.
198
+ #
195
199
  #: (untyped) -> Symbol?
196
200
  def deref_name(key)
197
201
  obj = deref(key)
@@ -214,7 +218,7 @@ class PDF::Reader
214
218
  #
215
219
  # Guaranteed to only return an Integer or nil. If the dereference results in
216
220
  # any other type then a MalformedPDFError exception will raise. Useful when
217
- # expecting an Array and no other type will do.
221
+ # expecting an Integer and no other type will do.
218
222
  #
219
223
  # Some effort to cast to an int is made when the reference points to a non-integer.
220
224
  #: (untyped) -> Integer?
@@ -239,7 +243,7 @@ class PDF::Reader
239
243
  #
240
244
  # Guaranteed to only return a Numeric or nil. If the dereference results in
241
245
  # any other type then a MalformedPDFError exception will raise. Useful when
242
- # expecting an Array and no other type will do.
246
+ # expecting a Number and no other type will do.
243
247
  #
244
248
  # Some effort to cast to a number is made when the reference points to a non-number.
245
249
  #: (untyped) -> Numeric?
@@ -267,6 +271,7 @@ class PDF::Reader
267
271
  # Guaranteed to only return a PDF::Reader::Stream or nil. If the dereference results in
268
272
  # any other type then a MalformedPDFError exception will raise. Useful when
269
273
  # expecting a stream and no other type will do.
274
+ #
270
275
  #: (untyped) -> PDF::Reader::Stream?
271
276
  def deref_stream(key)
272
277
  obj = deref(key)
@@ -553,7 +558,7 @@ class PDF::Reader
553
558
  def fetch_object(key)
554
559
  if xref[key].is_a?(Integer)
555
560
  buf = new_buffer(xref[key])
556
- decrypt(key, Parser.new(buf, self).object(key.id, key.gen))
561
+ decrypt(key, Parser.new(buf, objects: self).object(key.id, key.gen))
557
562
  end
558
563
  end
559
564
 
@@ -703,9 +708,9 @@ class PDF::Reader
703
708
  version.to_f
704
709
  end
705
710
 
706
- #: (IO | Tempfile | StringIO | String) -> (IO | Tempfile | StringIO)
711
+ #: (untyped) -> untyped
707
712
  def extract_io_from(input)
708
- if input.is_a?(IO) || input.is_a?(StringIO) || input.is_a?(Tempfile)
713
+ if input.respond_to?(:read) && input.respond_to?(:seek)
709
714
  input
710
715
  elsif File.file?(input.to_s)
711
716
  StringIO.new read_as_binary(input.to_s)
@@ -274,10 +274,10 @@ module PDF
274
274
  #: (Array[untyped], String) -> void
275
275
  def content_stream(receivers, instructions)
276
276
  buffer = Buffer.new(StringIO.new(instructions), :content_stream => true)
277
- parser = Parser.new(buffer, @objects)
277
+ parser = Parser.new(buffer, operators: PagesStrategy::OPERATORS, objects: @objects)
278
278
  params = []
279
279
 
280
- while (token = parser.parse_token(PagesStrategy::OPERATORS))
280
+ while (token = parser.parse_token)
281
281
  if token.kind_of?(Token) && method_name = PagesStrategy::OPERATORS[token]
282
282
  callback(receivers, method_name, params)
283
283
  params.clear
@@ -122,7 +122,7 @@ class PDF::Reader
122
122
 
123
123
  #: (untyped, untyped, untyped) -> untyped
124
124
  def local_string_insert(haystack, needle, index)
125
- haystack[Range.new(index, index + needle.length - 1)] = String.new(needle)
125
+ haystack[index, needle.length] = needle
126
126
  end
127
127
 
128
128
  #: (untyped) -> untyped
@@ -119,9 +119,9 @@ class PDF::Reader
119
119
 
120
120
  def font_size
121
121
  @font_size ||= begin
122
- _, zero = trm_transform(0,0)
123
- _, one = trm_transform(1,1)
124
- (zero - one).abs.round(10)
122
+ bl = trm_transform_point(Point::ZERO_ZERO)
123
+ tr = trm_transform_point(Point::ONE_ONE)
124
+ (bl.y - tr.y).abs.round(10)
125
125
  end
126
126
  end
127
127
 
@@ -146,14 +146,7 @@ class PDF::Reader
146
146
  #####################################################
147
147
 
148
148
  def move_text_position(x, y) # Td
149
- temp = TransformationMatrix.new(1, 0,
150
- 0, 1,
151
- x, y)
152
- @text_line_matrix = temp.multiply!(
153
- @text_line_matrix.a, @text_line_matrix.b,
154
- @text_line_matrix.c, @text_line_matrix.d,
155
- @text_line_matrix.e, @text_line_matrix.f
156
- )
149
+ @text_line_matrix.prepend_translation!(x, y)
157
150
  @text_matrix = @text_line_matrix.dup
158
151
  @font_size = @text_rendering_matrix = nil # invalidate cached value
159
152
  end
@@ -227,28 +220,83 @@ class PDF::Reader
227
220
  # transform x and y co-ordinates from the current user space to the
228
221
  # underlying device space.
229
222
  #
223
+ # Deprecated. Prefer ctm_transform_point()
224
+ #
225
+ #: (Numeric, Numeric) -> [Numeric]
230
226
  def ctm_transform(x, y)
227
+ # TODO print a deprecation warning
228
+ res = ctm_transform_point(x, y)
231
229
  [
232
- (ctm.a * x) + (ctm.c * y) + (ctm.e),
233
- (ctm.b * x) + (ctm.d * y) + (ctm.f)
230
+ res.x,
231
+ res.y
234
232
  ]
235
233
  end
236
234
 
235
+ # transform x and y co-ordinates from the current user space to the
236
+ # underlying device space.
237
+ #
238
+ # Recommended usage:
239
+ #
240
+ # ctm_transform_point(Point.new(0,1))
241
+ #
242
+ # Also supported:
243
+ #
244
+ # ctm_transform_point(0,1)
245
+ #
246
+ #: (PDF::Reader::Point | Numeric, ?Numeric) -> PDF::Reader::Point
247
+ def ctm_transform_point(pt, opt_y = -1)
248
+ unless pt.is_a?(Point)
249
+ pt = Point.new(pt.to_i, opt_y.to_i)
250
+ end
251
+ Point.new(
252
+ (ctm.a * pt.x) + (ctm.c * pt.y) + (ctm.e),
253
+ (ctm.b * pt.x) + (ctm.d * pt.y) + (ctm.f)
254
+ )
255
+ end
256
+
237
257
  # transform x and y co-ordinates from the current text space to the
238
258
  # underlying device space.
239
259
  #
260
+ # Deprecated. Prefer trm_transform_point()
261
+ #
262
+ #: (Numeric, Numeric) -> Array[Numeric]
263
+ def trm_transform(x, y)
264
+ # TODO print a deprecation warning
265
+ res = trm_transform_point(x, y)
266
+ [
267
+ res.x,
268
+ res.y
269
+ ]
270
+ end
271
+
272
+ # transform x and y co-ordinates from the current text space to the
273
+ # underlying device space.
274
+ #
275
+ # Recommended usage:
276
+ #
277
+ # trm_transform_point(Point.new(0,1))
278
+ #
279
+ # Also supported:
280
+ #
281
+ # trm_transform_point(0,1)
282
+ #
240
283
  # transforming (0,0) is a really common case, so optimise for it to
241
284
  # avoid unnecessary object allocations
242
285
  #
243
- def trm_transform(x, y)
286
+ #: (PDF::Reader::Point | Numeric, ?Numeric) -> PDF::Reader::Point
287
+ def trm_transform_point(pt, opt_y = -1)
288
+ unless pt.is_a?(Point)
289
+ pt = Point.new(pt.to_i, opt_y.to_i)
290
+ end
291
+
244
292
  trm = text_rendering_matrix
245
- if x == 0 && y == 0
246
- [trm.e, trm.f]
293
+ if pt.x == 0 && pt.y == 0
294
+ Point.new(trm.e, trm.f)
247
295
  else
248
- [
249
- (trm.a * x) + (trm.c * y) + (trm.e),
250
- (trm.b * x) + (trm.d * y) + (trm.f)
251
- ]
296
+ Point.new(
297
+ (trm.a * pt.x) + (trm.c * pt.y) + (trm.e),
298
+ (trm.b * pt.x) + (trm.d * pt.y) + (trm.f)
299
+ )
252
300
  end
253
301
  end
254
302
 
@@ -342,15 +390,11 @@ class PDF::Reader
342
390
  end
343
391
  # TODO: support ty > 0
344
392
  ty = 0
345
- temp = TransformationMatrix.new(1, 0,
346
- 0, 1,
347
- tx, ty)
348
- @text_matrix = temp.multiply!(
349
- @text_matrix.a, @text_matrix.b,
350
- @text_matrix.c, @text_matrix.d,
351
- @text_matrix.e, @text_matrix.f
352
- )
353
- @font_size = @text_rendering_matrix = nil # invalidate cached value
393
+ @text_matrix.prepend_translation!(tx, ty)
394
+ @text_rendering_matrix = nil # invalidate cached value
395
+ # we used to invalidate @font_size here too, but it's not required
396
+ # font_size depends only on trm.b/trm.d (scale/rotation), not trm.e/f (translation),
397
+ # so it remains valid after a glyph displacement and does not need invalidation
354
398
  end
355
399
 
356
400
  private
@@ -49,6 +49,8 @@ module PDF
49
49
  @page = page
50
50
  @content = []
51
51
  @characters = []
52
+ @actual_text = nil
53
+ @actual_text_consumed = false
52
54
  end
53
55
 
54
56
  def runs(opts = {})
@@ -84,6 +86,7 @@ module PDF
84
86
  end
85
87
 
86
88
  # deprecated
89
+ #: () -> String
87
90
  def content
88
91
  mediabox = @page.rectangles[:MediaBox]
89
92
  PageLayout.new(runs, mediabox).to_s
@@ -120,6 +123,21 @@ module PDF
120
123
  move_to_next_line_and_show_text(string)
121
124
  end
122
125
 
126
+ #####################################################
127
+ # Marked Content
128
+ #####################################################
129
+ def begin_marked_content_with_pl(tag, properties)
130
+ if properties.is_a?(Hash) && properties[:ActualText]
131
+ @actual_text = properties[:ActualText]
132
+ @actual_text_consumed = false
133
+ end
134
+ end
135
+
136
+ def end_marked_content
137
+ @actual_text = nil
138
+ @actual_text_consumed = false
139
+ end
140
+
123
141
  #####################################################
124
142
  # XObjects
125
143
  #####################################################
@@ -134,6 +152,7 @@ module PDF
134
152
 
135
153
  private
136
154
 
155
+ #: (String) -> void
137
156
  def internal_show_text(string)
138
157
  PDF::Reader::Error.validate_type_as_malformed(string, "string", String)
139
158
  if @state.current_font.nil?
@@ -142,41 +161,52 @@ module PDF
142
161
  glyphs = @state.current_font.unpack(string)
143
162
  glyphs.each_with_index do |glyph_code, index|
144
163
  # paint the current glyph
145
- newx, newy = @state.trm_transform(0,0)
146
- newx, newy = apply_rotation(newx, newy)
164
+ bl = @state.trm_transform_point(Point::ZERO_ZERO)
165
+ text_origin = apply_rotation(bl)
147
166
 
148
167
  utf8_chars = @state.current_font.to_utf8(glyph_code)
149
168
 
169
+ # Use ActualText from marked content if available (PDF 32000-1 §14.9.4).
170
+ # ActualText replaces all text within a BDC/EMC span.
171
+ if @actual_text
172
+ if !@actual_text_consumed
173
+ text = @actual_text
174
+ utf8_chars = PDF::Reader::EncodingUtils.string_to_utf8(text)
175
+ @actual_text_consumed = true
176
+ else
177
+ utf8_chars = ""
178
+ end
179
+ end
180
+
150
181
  # apply to glyph displacment for the current glyph so the next
151
182
  # glyph will appear in the correct position
152
183
  glyph_width = @state.current_font.glyph_width_in_text_space(glyph_code)
153
184
  th = 1
154
185
  scaled_glyph_width = glyph_width * @state.font_size * th
155
186
  unless utf8_chars == SPACE
156
- @characters << TextRun.new(newx, newy, scaled_glyph_width, @state.font_size, utf8_chars)
187
+ @characters << TextRun.new(text_origin.x, text_origin.y, scaled_glyph_width, @state.font_size, utf8_chars)
157
188
  end
158
189
  @state.process_glyph_displacement(glyph_width, 0, utf8_chars == SPACE)
159
190
  end
160
191
  end
161
192
 
162
- def apply_rotation(x, y)
193
+ #: (Point) -> Point
194
+ def apply_rotation(pt)
163
195
  if @page.rotate == 90
164
- tmp = x
165
- x = y
166
- y = tmp * -1
196
+ Point.new(pt.y, pt.x * -1)
167
197
  elsif @page.rotate == 180
168
- y *= -1
169
- x *= -1
198
+ Point.new(pt.x * -1, pt.y * -1)
170
199
  elsif @page.rotate == 270
171
- tmp = y
172
- y = x
173
- x = tmp * -1
200
+ Point.new(pt.y * -1, pt.x)
201
+ else
202
+ pt
174
203
  end
175
- return x, y
176
204
  end
177
205
 
178
206
  # take a collection of TextRun objects and merge any that are in close
179
207
  # proximity
208
+ #
209
+ #: (Array[PDF::Reader::TextRun]) -> Array[PDF::Reader::TextRun]
180
210
  def merge_runs(runs)
181
211
  runs.group_by { |char|
182
212
  char.y.to_i
@@ -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
  ################################################################################
@@ -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