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.
- checksums.yaml +4 -4
- data/CHANGELOG +24 -1
- 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 +6 -4
- 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 +29 -18
- 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 +118 -34
- metadata +6 -18
|
@@ -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(
|
|
106
|
+
parser = Parser.new(
|
|
107
|
+
buffer,
|
|
108
|
+
operators: PagesStrategy::OPERATORS, objects: @objects
|
|
109
|
+
)
|
|
107
110
|
result = []
|
|
108
|
-
while (token = parser.parse_token
|
|
111
|
+
while (token = parser.parse_token)
|
|
109
112
|
result << token
|
|
110
113
|
end
|
|
111
114
|
result
|
|
@@ -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
|
-
#:
|
|
20
|
+
#: Integer
|
|
21
21
|
attr_reader :hits
|
|
22
22
|
|
|
23
|
-
#:
|
|
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
|
|
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
|
-
#: (
|
|
62
|
+
#: (untyped, ?Hash[Symbol, untyped]) -> void
|
|
63
63
|
def initialize(input, opts = {})
|
|
64
|
-
@io = extract_io_from(input) #:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
#: (
|
|
711
|
+
#: (untyped) -> untyped
|
|
707
712
|
def extract_io_from(input)
|
|
708
|
-
if input.
|
|
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)
|
data/lib/pdf/reader/page.rb
CHANGED
|
@@ -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
|
|
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[
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
(
|
|
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
|
-
|
|
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
|
-
|
|
233
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
|
|
146
|
-
|
|
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(
|
|
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
|
-
|
|
193
|
+
#: (Point) -> Point
|
|
194
|
+
def apply_rotation(pt)
|
|
163
195
|
if @page.rotate == 90
|
|
164
|
-
|
|
165
|
-
x = y
|
|
166
|
-
y = tmp * -1
|
|
196
|
+
Point.new(pt.y, pt.x * -1)
|
|
167
197
|
elsif @page.rotate == 180
|
|
168
|
-
y
|
|
169
|
-
x *= -1
|
|
198
|
+
Point.new(pt.x * -1, pt.y * -1)
|
|
170
199
|
elsif @page.rotate == 270
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
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
|
|