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.
@@ -97,12 +97,21 @@ class PDF::Reader
97
97
  extract_descriptor(obj)
98
98
  extract_descendants(obj)
99
99
  @width_calc = build_width_calculator #: widthCalculator
100
+ @utf8_cache = {} #: Hash[Integer, String]
100
101
  end
101
102
 
102
103
  #: (Integer | String | Array[Integer | String]) -> String
103
104
  def to_utf8(params)
104
105
  if @tounicode
105
- to_utf8_via_cmap(params, @tounicode)
106
+ if params.is_a?(Integer)
107
+ cached = @utf8_cache[params]
108
+ return cached unless cached.nil?
109
+ result = to_utf8_via_cmap(params, @tounicode)
110
+ @utf8_cache[params] = result
111
+ result
112
+ else
113
+ to_utf8_via_cmap(params, @tounicode)
114
+ end
106
115
  else
107
116
  to_utf8_via_encoding(params)
108
117
  end
@@ -134,9 +143,9 @@ class PDF::Reader
134
143
  glyph_width_in_glyph_space = glyph_width(code_point)
135
144
 
136
145
  if @subtype == :Type3
137
- x1, _y1 = font_matrix_transform(0,0)
138
- x2, _y2 = font_matrix_transform(glyph_width_in_glyph_space, 0)
139
- (x2 - x1).abs.round(2)
146
+ pt1 = font_matrix_transform(Point::ZERO_ZERO)
147
+ pt2 = font_matrix_transform(Point.new(glyph_width_in_glyph_space, 0))
148
+ (pt2.x - pt1.x).abs.round(2)
140
149
  else
141
150
  glyph_width_in_glyph_space / 1000.0
142
151
  end
@@ -145,9 +154,13 @@ class PDF::Reader
145
154
  private
146
155
 
147
156
  # Only valid for Type3 fonts
148
- #: (Numeric, Numeric) -> [Numeric, Numeric]
149
- def font_matrix_transform(x, y)
150
- return x, y if @font_matrix.nil?
157
+ #
158
+ # transforming (0,0) is a really common case, so optimise for it to
159
+ # avoid unnecessary object allocations
160
+ #
161
+ #: (PDF::Reader::Point) -> PDF::Reader::Point
162
+ def font_matrix_transform(pt)
163
+ return pt if @font_matrix.nil?
151
164
 
152
165
  matrix = TransformationMatrix.new(
153
166
  @font_matrix[0] || 0, @font_matrix[1] || 0,
@@ -155,13 +168,13 @@ class PDF::Reader
155
168
  @font_matrix[4] || 0, @font_matrix[5] || 0,
156
169
  )
157
170
 
158
- if x == 0 && y == 0
159
- [matrix.e, matrix.f]
171
+ if pt.x == 0 && pt.y == 0
172
+ Point.new(matrix.e, matrix.f)
160
173
  else
161
- [
162
- (matrix.a * x) + (matrix.c * y) + (matrix.e),
163
- (matrix.b * x) + (matrix.d * y) + (matrix.f)
164
- ]
174
+ Point.new(
175
+ (matrix.a * pt.x) + (matrix.c * pt.y) + (matrix.e),
176
+ (matrix.b * pt.x) + (matrix.d * pt.y) + (matrix.f)
177
+ )
165
178
  end
166
179
  end
167
180
 
@@ -235,9 +248,17 @@ class PDF::Reader
235
248
 
236
249
  if obj[:ToUnicode]
237
250
  # ToUnicode is optional for Type1 and Type3
238
- stream = @ohash.deref_stream(obj[:ToUnicode])
239
- if stream
240
- @tounicode = PDF::Reader::CMap.new(stream.unfiltered_data)
251
+ begin
252
+ stream = @ohash.deref_stream(obj[:ToUnicode])
253
+ if stream
254
+ @tounicode = PDF::Reader::CMap.new(stream.unfiltered_data)
255
+ end
256
+ rescue PDF::Reader::MalformedPDFError => e
257
+ # If ToUnicode exists but isn't a stream, lets pretend it didn't exist. We
258
+ # can't do anything with it unless it's a stream with a CMap
259
+ unless e.message == "expected object to be a Stream or nil"
260
+ raise e
261
+ end
241
262
  end
242
263
  end
243
264
  end
@@ -282,13 +303,11 @@ class PDF::Reader
282
303
  def to_utf8_via_cmap(params, cmap)
283
304
  case params
284
305
  when Integer
285
- [
286
- cmap.decode(params)
287
- ].flatten.pack("U*")
306
+ cmap.decode(params).pack("U*")
288
307
  when String
289
- unpack_string_to_array_of_ints(params, encoding.unpack).map { |code_point|
308
+ unpack_string_to_array_of_ints(params, encoding.unpack).flat_map { |code_point|
290
309
  cmap.decode(code_point)
291
- }.flatten.pack("U*")
310
+ }.pack("U*")
292
311
  when Array
293
312
  params.collect { |param| to_utf8_via_cmap(param, cmap) }.join("")
294
313
  end
@@ -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
  ################################################################################