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/font.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
(
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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).
|
|
308
|
+
unpack_string_to_array_of_ints(params, encoding.unpack).flat_map { |code_point|
|
|
290
309
|
cmap.decode(code_point)
|
|
291
|
-
}.
|
|
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(
|
|
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
|