qr_reader 0.0.1
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 +7 -0
- data/CHANGELOG.md +16 -0
- data/LICENSE.txt +21 -0
- data/NOTICE +14 -0
- data/README.md +222 -0
- data/exe/qr_reader +66 -0
- data/lib/qr_reader/binarizer.rb +170 -0
- data/lib/qr_reader/bit_matrix.rb +120 -0
- data/lib/qr_reader/decoder/bitstream_parser.rb +307 -0
- data/lib/qr_reader/decoder/character_set.rb +163 -0
- data/lib/qr_reader/decoder/codewords.rb +164 -0
- data/lib/qr_reader/decoder/data_mask.rb +57 -0
- data/lib/qr_reader/decoder/format_reader.rb +204 -0
- data/lib/qr_reader/decoder/function_patterns.rb +62 -0
- data/lib/qr_reader/decoder/galois_field.rb +69 -0
- data/lib/qr_reader/decoder/reed_solomon.rb +312 -0
- data/lib/qr_reader/decoder.rb +241 -0
- data/lib/qr_reader/detector/alignment_grid.rb +127 -0
- data/lib/qr_reader/detector/alignment_pattern_finder.rb +191 -0
- data/lib/qr_reader/detector/candidate/dimension.rb +237 -0
- data/lib/qr_reader/detector/candidate/strokes.rb +126 -0
- data/lib/qr_reader/detector/candidate.rb +436 -0
- data/lib/qr_reader/detector/finder_pattern.rb +63 -0
- data/lib/qr_reader/detector/finder_pattern_finder.rb +199 -0
- data/lib/qr_reader/detector/geometry.rb +118 -0
- data/lib/qr_reader/detector/grid_sampler.rb +350 -0
- data/lib/qr_reader/detector/pattern_sets.rb +235 -0
- data/lib/qr_reader/detector/perspective_transform.rb +117 -0
- data/lib/qr_reader/detector/pure_detector.rb +197 -0
- data/lib/qr_reader/detector.rb +215 -0
- data/lib/qr_reader/errors.rb +14 -0
- data/lib/qr_reader/format_information.rb +79 -0
- data/lib/qr_reader/formats/bmp.rb +434 -0
- data/lib/qr_reader/formats/ccitt.rb +81 -0
- data/lib/qr_reader/formats/gif.rb +272 -0
- data/lib/qr_reader/formats/jpeg.rb +152 -0
- data/lib/qr_reader/formats/pdf.rb +2935 -0
- data/lib/qr_reader/formats/png.rb +311 -0
- data/lib/qr_reader/formats/pnm.rb +286 -0
- data/lib/qr_reader/formats/tiff.rb +1345 -0
- data/lib/qr_reader/formats.rb +279 -0
- data/lib/qr_reader/image.rb +231 -0
- data/lib/qr_reader/kernels/binarizer.rb +475 -0
- data/lib/qr_reader/kernels/ccitt.rb +554 -0
- data/lib/qr_reader/kernels/detector.rb +587 -0
- data/lib/qr_reader/kernels/image.rb +65 -0
- data/lib/qr_reader/kernels/jpeg.rb +3421 -0
- data/lib/qr_reader/kernels/lzw.rb +173 -0
- data/lib/qr_reader/kernels/pixels.rb +116 -0
- data/lib/qr_reader/kernels/png.rb +1138 -0
- data/lib/qr_reader/kernels.rb +14 -0
- data/lib/qr_reader/native_kernels.rb +130 -0
- data/lib/qr_reader/reader.rb +348 -0
- data/lib/qr_reader/result.rb +49 -0
- data/lib/qr_reader/symbol_version.rb +173 -0
- data/lib/qr_reader/version.rb +5 -0
- data/lib/qr_reader.rb +77 -0
- metadata +103 -0
|
@@ -0,0 +1,2935 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "strscan"
|
|
4
|
+
require "zlib"
|
|
5
|
+
require_relative "../kernels/lzw"
|
|
6
|
+
|
|
7
|
+
module QrReader
|
|
8
|
+
module Formats
|
|
9
|
+
# Extracts the raster images of PDF documents page by page: scanned
|
|
10
|
+
# pages, faxes and photos placed in PDFs. Vector graphics (including
|
|
11
|
+
# vector-drawn QR codes) are not rendered.
|
|
12
|
+
#
|
|
13
|
+
# PDF.each_image(bytes) { |image, info| ... } # info: {page: 0, name: "Im0", width: 2480, height: 3508,
|
|
14
|
+
# # pixel_aspect: 1.0, dpi: [300.0, 300.0]}
|
|
15
|
+
# PDF.each_image(bytes) # => Enumerator
|
|
16
|
+
# PDF.each_image(bytes, raw_jpeg: true) { |image_or_jpeg, info| ... }
|
|
17
|
+
# PDF.images(bytes) # => [#<QrReader::Image 2480x3508>, ...]
|
|
18
|
+
#
|
|
19
|
+
# Structure: the header may sit anywhere in the first KB; classic
|
|
20
|
+
# cross-reference tables and cross-reference streams (incremental
|
|
21
|
+
# updates, hybrid files), object streams; when the cross-reference data
|
|
22
|
+
# is broken or missing, the file is scanned for "N G obj". Objects are
|
|
23
|
+
# parsed and streams decoded only when needed. Pages are walked through
|
|
24
|
+
# the page tree (inherited resources) and each page's content streams
|
|
25
|
+
# are run to find the images it draws, in drawing order: image
|
|
26
|
+
# XObjects (also inside form XObjects) and inline images
|
|
27
|
+
# (BI ... ID ... EI). Images are not rendered onto a page: each one is
|
|
28
|
+
# yielded as it is stored (an image drawn several times on a page is
|
|
29
|
+
# yielded once), except that bands of one picture - images drawn
|
|
30
|
+
# directly above each other with the same width and scale, as printer
|
|
31
|
+
# drivers write them - are joined into one image. +info[:name]+ is the
|
|
32
|
+
# (first) XObject's resource name (nil for inline images),
|
|
33
|
+
# +info[:page]+ the 0-based page index. +info[:pixel_aspect]+ is the
|
|
34
|
+
# width of an image pixel divided by its height as the page draws it
|
|
35
|
+
# (from the transformation matrix: 0.48 for a 204 x 98 dpi fax drawn
|
|
36
|
+
# at its true size; 1.0 when unknown), +info[:dpi]+ its resolution on
|
|
37
|
+
# the page ([x, y], nil when unknown).
|
|
38
|
+
#
|
|
39
|
+
# With +raw_jpeg: true+, DCT images whose luminance is exactly what
|
|
40
|
+
# JPEG.decode makes of their data (1 or 3 components with the default
|
|
41
|
+
# /Decode, or Adobe CMYK with an inverting one) are not decoded: the
|
|
42
|
+
# block gets the JPEG data (a binary String) in place of the Image,
|
|
43
|
+
# with +info[:format]+ :jpeg and the size of the JPEG frame, so that a
|
|
44
|
+
# caller can decode it at reduced size first (JPEG.decode(data,
|
|
45
|
+
# scale:)). JPEG.decode of such data may still raise ImageError.
|
|
46
|
+
#
|
|
47
|
+
# Images: 1/2/4/8/16 bits per component; DeviceGray/RGB/CMYK, CalGray,
|
|
48
|
+
# CalRGB, ICCBased (by /N), Indexed, Separation and DeviceN (tint =
|
|
49
|
+
# darkness), Lab (L*), named colour spaces; stencil masks; /Decode
|
|
50
|
+
# arrays. Filters: FlateDecode and LZWDecode (with PNG and TIFF
|
|
51
|
+
# predictors), RunLengthDecode, ASCIIHexDecode, ASCII85Decode,
|
|
52
|
+
# DCTDecode (JPEG), CCITTFaxDecode and JBIG2Decode (generic regions;
|
|
53
|
+
# symbol, halftone and refinement coding are skipped). JPEG 2000 images
|
|
54
|
+
# are skipped. Colour is reduced to luminance with Image.luma; alpha
|
|
55
|
+
# (/SMask) is ignored.
|
|
56
|
+
#
|
|
57
|
+
# Encrypted documents are decrypted when their user password is empty
|
|
58
|
+
# (standard security handler: RC4, AES-128, AES-256); other encrypted
|
|
59
|
+
# documents raise UnsupportedImageError.
|
|
60
|
+
#
|
|
61
|
+
# Images that cannot be decoded are skipped. Data that is not a PDF
|
|
62
|
+
# raises ImageError; a document without any decodable image raises
|
|
63
|
+
# UnsupportedImageError (ImageError when its images are corrupt).
|
|
64
|
+
module PDF
|
|
65
|
+
# Raised internally for malformed syntax.
|
|
66
|
+
class ParseError < ImageError; end
|
|
67
|
+
|
|
68
|
+
# An indirect reference "num gen R".
|
|
69
|
+
Ref = Data.define(:num, :gen)
|
|
70
|
+
# A bare keyword or operator met while parsing an object.
|
|
71
|
+
Keyword = Data.define(:word)
|
|
72
|
+
# A stream object: its dictionary and raw (encoded, possibly
|
|
73
|
+
# encrypted) data.
|
|
74
|
+
Stream = Struct.new(:dict, :raw, :num, :gen)
|
|
75
|
+
# A page: its dictionary and its (possibly inherited) resources; or,
|
|
76
|
+
# for documents without usable pages, a lone image stream.
|
|
77
|
+
Page = Struct.new(:dict, :resources, :orphan)
|
|
78
|
+
# An image found on a page: an image XObject (+stream+) or an inline
|
|
79
|
+
# image (+data+), with the resources that apply to it and the
|
|
80
|
+
# current transformation matrix it was drawn with.
|
|
81
|
+
ImageSource = Struct.new(:dict, :stream, :data, :resources, :name, :ctm)
|
|
82
|
+
# A DCT image handed over undecoded (each_image with raw_jpeg: true):
|
|
83
|
+
# the JPEG data and its frame size.
|
|
84
|
+
RawJpeg = Struct.new(:data, :width, :height)
|
|
85
|
+
|
|
86
|
+
# Largest decoded stream we produce for anything but image samples.
|
|
87
|
+
STREAM_LIMIT = 1 << 26
|
|
88
|
+
# Largest decoded image sample buffer.
|
|
89
|
+
SAMPLE_LIMIT = 1 << 28
|
|
90
|
+
|
|
91
|
+
# Errors that garbage input could trigger deep inside the decoders.
|
|
92
|
+
INTERNAL_ERRORS = [NoMethodError, TypeError, ArgumentError, RangeError, IndexError, ZeroDivisionError,
|
|
93
|
+
EncodingError, RegexpError, Zlib::Error].freeze
|
|
94
|
+
|
|
95
|
+
class << self
|
|
96
|
+
# Yields each raster image of the document with an info Hash
|
|
97
|
+
# ({page:, name:, width:, height:}), page by page in drawing order.
|
|
98
|
+
# Returns an Enumerator without a block.
|
|
99
|
+
def each_image(bytes, raw_jpeg: false)
|
|
100
|
+
return enum_for(:each_image, bytes, raw_jpeg: raw_jpeg) unless block_given?
|
|
101
|
+
|
|
102
|
+
document = guard { Document.new(binary(bytes)) }
|
|
103
|
+
pages = guard { document.pages }
|
|
104
|
+
failures = []
|
|
105
|
+
count = 0
|
|
106
|
+
pages.each_with_index do |page, index|
|
|
107
|
+
band = nil # the latest image, held back in case the next one continues it
|
|
108
|
+
sources(document, page, failures).each do |source|
|
|
109
|
+
part = decode(document, source, failures, raw_jpeg) or next
|
|
110
|
+
next if band&.join(part, source.ctm)
|
|
111
|
+
|
|
112
|
+
if band && (image = band.image)
|
|
113
|
+
count += 1
|
|
114
|
+
yield image, band.info(index, image)
|
|
115
|
+
end
|
|
116
|
+
band = Band.new([part], source.ctm, source.name)
|
|
117
|
+
end
|
|
118
|
+
next unless band && (image = band.image)
|
|
119
|
+
|
|
120
|
+
count += 1
|
|
121
|
+
yield image, band.info(index, image)
|
|
122
|
+
end
|
|
123
|
+
raise no_images(failures) if count.zero?
|
|
124
|
+
|
|
125
|
+
nil
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# All raster images of the document (see #each_image).
|
|
129
|
+
def images(bytes)
|
|
130
|
+
each_image(bytes).map { |image, _info| image }
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
private
|
|
134
|
+
|
|
135
|
+
def binary(bytes)
|
|
136
|
+
raise ImageError, "expected a String of PDF data, got #{bytes.class}" unless bytes.is_a?(String)
|
|
137
|
+
|
|
138
|
+
bytes.encoding == Encoding::BINARY ? bytes : bytes.b
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def guard
|
|
142
|
+
yield
|
|
143
|
+
rescue *INTERNAL_ERRORS => e
|
|
144
|
+
raise ImageError, "corrupt PDF (#{e.class}: #{e.message})"
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def sources(document, page, failures)
|
|
148
|
+
document.page_images(page)
|
|
149
|
+
rescue Error, *INTERNAL_ERRORS => e
|
|
150
|
+
failures << e
|
|
151
|
+
[]
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def decode(document, source, failures, raw_jpeg)
|
|
155
|
+
ImageDecoder.new(document, source, raw_jpeg).decode
|
|
156
|
+
rescue Error, *INTERNAL_ERRORS => e
|
|
157
|
+
failures << e
|
|
158
|
+
nil
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def no_images(failures)
|
|
162
|
+
return UnsupportedImageError.new("PDF contains no raster images") if failures.empty?
|
|
163
|
+
|
|
164
|
+
reason = failures.first.message
|
|
165
|
+
if failures.any?(UnsupportedImageError)
|
|
166
|
+
UnsupportedImageError.new("PDF contains no decodable raster image (#{reason})")
|
|
167
|
+
else
|
|
168
|
+
ImageError.new("PDF contains no decodable raster image (#{reason})")
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Images drawn directly above or below each other with the same width
|
|
174
|
+
# and scale are bands of one picture (scanner drivers that print
|
|
175
|
+
# through PostScript cut page images into strips), joined so that a
|
|
176
|
+
# code across a band boundary stays readable. +parts+ (Images or
|
|
177
|
+
# RawJpegs) are top to bottom; +ctm+ maps the unit square onto the
|
|
178
|
+
# joined picture.
|
|
179
|
+
Band = Struct.new(:parts, :ctm, :name) do
|
|
180
|
+
# The picture: an Image, the JPEG data of a lone RawJpeg, or nil
|
|
181
|
+
# when a joined RawJpeg cannot be decoded.
|
|
182
|
+
def image
|
|
183
|
+
part = parts.first
|
|
184
|
+
return part.is_a?(RawJpeg) ? part.data : part if parts.size == 1
|
|
185
|
+
|
|
186
|
+
images = parts.map { |p| p.is_a?(RawJpeg) ? JPEG.decode(p.data) : p }
|
|
187
|
+
Image.new(images.first.width, images.sum(&:height), images.map(&:data).join)
|
|
188
|
+
rescue Error
|
|
189
|
+
nil
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# The info Hash for +image+ (see PDF.each_image).
|
|
193
|
+
def info(page, image)
|
|
194
|
+
info = { page: page, name: name }
|
|
195
|
+
if image.is_a?(String)
|
|
196
|
+
info[:width] = parts.first.width
|
|
197
|
+
info[:height] = parts.first.height
|
|
198
|
+
info[:format] = :jpeg
|
|
199
|
+
else
|
|
200
|
+
info[:width] = image.width
|
|
201
|
+
info[:height] = image.height
|
|
202
|
+
end
|
|
203
|
+
info.merge!(Band.geometry(ctm, info[:width], info[:height]))
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# {pixel_aspect:, dpi:} of a +width+ x +height+ image drawn with
|
|
207
|
+
# +ctm+ (which maps the unit square onto the drawn image).
|
|
208
|
+
def self.geometry(ctm, width, height)
|
|
209
|
+
a, b, c, d = ctm
|
|
210
|
+
drawn_w = ctm && Math.hypot(a, b) # in points (1/72 inch)
|
|
211
|
+
drawn_h = ctm && Math.hypot(c, d)
|
|
212
|
+
return { pixel_aspect: 1.0, dpi: nil } unless drawn_w&.finite? && drawn_h&.finite? &&
|
|
213
|
+
drawn_w > 1e-6 && drawn_h > 1e-6
|
|
214
|
+
|
|
215
|
+
aspect = (drawn_w / width) / (drawn_h / height)
|
|
216
|
+
aspect = 1.0 unless aspect.between?(0.125, 8.0)
|
|
217
|
+
{ pixel_aspect: aspect.round(6), dpi: [(width * 72 / drawn_w).round(2), (height * 72 / drawn_h).round(2)] }
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Appends +image+ (drawn with +matrix+) if it continues this band;
|
|
221
|
+
# returns true if it did.
|
|
222
|
+
def join(image, matrix)
|
|
223
|
+
return false unless ctm && matrix && upright?(ctm) && upright?(matrix)
|
|
224
|
+
|
|
225
|
+
width = parts.first.width
|
|
226
|
+
height = parts.sum(&:height)
|
|
227
|
+
a, _, _, d, e, f = ctm
|
|
228
|
+
a2, _, _, d2, e2, f2 = matrix
|
|
229
|
+
row = d / height
|
|
230
|
+
return false unless image.width == width && (height + image.height) * width <= MAX_PIXELS
|
|
231
|
+
# Same horizontal placement (within half a pixel) and row height.
|
|
232
|
+
return false unless (a2 - a).abs <= a * 0.01 && (e2 - e).abs <= a / width / 2
|
|
233
|
+
return false unless (d2 / image.height - row).abs <= row * 0.01
|
|
234
|
+
|
|
235
|
+
if (f2 + d2 - f).abs <= row / 2 # directly below
|
|
236
|
+
parts << image
|
|
237
|
+
self.ctm = [a, 0.0, 0.0, d + d2, e, f2]
|
|
238
|
+
elsif (f + d - f2).abs <= row / 2 # directly above
|
|
239
|
+
parts.unshift(image)
|
|
240
|
+
self.ctm = [a, 0.0, 0.0, d + d2, e, f]
|
|
241
|
+
else
|
|
242
|
+
return false
|
|
243
|
+
end
|
|
244
|
+
true
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
private
|
|
248
|
+
|
|
249
|
+
def upright?(m)
|
|
250
|
+
m[1].abs < 1e-6 && m[2].abs < 1e-6 && m[0].positive? && m[3].positive?
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Small conversions shared by the parts below.
|
|
255
|
+
module Util
|
|
256
|
+
module_function
|
|
257
|
+
|
|
258
|
+
def int(value)
|
|
259
|
+
case value
|
|
260
|
+
when Integer then value
|
|
261
|
+
when Float then value.finite? ? value.to_i : nil
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def positive(value)
|
|
266
|
+
v = int(value)
|
|
267
|
+
v if v&.positive?
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def number(value)
|
|
271
|
+
case value
|
|
272
|
+
when Integer then value.to_f
|
|
273
|
+
when Float then value.finite? ? value : nil
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# Big-endian unsigned integer of +width+ bytes at +pos+.
|
|
278
|
+
def be_int(data, pos, width)
|
|
279
|
+
v = 0
|
|
280
|
+
i = 0
|
|
281
|
+
while i < width
|
|
282
|
+
v = (v << 8) | data.getbyte(pos + i)
|
|
283
|
+
i += 1
|
|
284
|
+
end
|
|
285
|
+
v
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Tokenizer and object parser (ISO 32000-1, 7.2 and 7.3).
|
|
290
|
+
#
|
|
291
|
+
# Regexps never match unbounded runs: Onigmo keeps a backtracking
|
|
292
|
+
# stack entry (about 50 bytes) per character a quantifier matches,
|
|
293
|
+
# gigabytes for the long runs of white space or token characters in
|
|
294
|
+
# a hostile file or stream. White space and tokens are scanned in
|
|
295
|
+
# bounded steps (the rest of an overlong token is skipped), comments
|
|
296
|
+
# with String#index.
|
|
297
|
+
class Parser
|
|
298
|
+
MAX_DEPTH = 100
|
|
299
|
+
# Array elements and dictionary entries one document may parse
|
|
300
|
+
# (about 100 bytes of memory and 2 us each at worst, e.g. empty
|
|
301
|
+
# strings; a scanned page needs about 30).
|
|
302
|
+
MAX_ELEMENTS = 2_000_000
|
|
303
|
+
NOT_SPACE = /[^\x00\t\n\f\r ]/n
|
|
304
|
+
LINE_END = /[\r\n]/n
|
|
305
|
+
SPACE_RUN = /[\x00\t\n\f\r ]{1,1024}/n
|
|
306
|
+
TOKEN_RUN = %r{[^\x00\t\n\f\r ()<>\[\]{}/%]{1,1024}}n
|
|
307
|
+
NAME_RUN = %r{/[^\x00\t\n\f\r ()<>\[\]{}/%]{0,1024}}n
|
|
308
|
+
# Ends a regular token (white space or a delimiter).
|
|
309
|
+
DELIMITER = %r{[\x00\t\n\f\r ()<>\[\]{}/%]}n
|
|
310
|
+
DELIMITERS = "\x00\t\n\f\r ()<>[]{}/%".b.bytes.freeze
|
|
311
|
+
# Byte -> true for white space / for white space and delimiters.
|
|
312
|
+
SPACE_BYTE = Array.new(256) { |b| "\x00\t\n\f\r ".b.bytes.include?(b) }.freeze
|
|
313
|
+
DELIMITER_BYTE = Array.new(256) { |b| DELIMITERS.include?(b) }.freeze
|
|
314
|
+
INTEGER = /\A[+-]?\d+\z/
|
|
315
|
+
REF_TAIL = %r{[\x00\t\n\f\r ]{1,64}(\d{1,10})[\x00\t\n\f\r ]{1,64}R(?![^\x00\t\n\f\r ()<>\[\]{}/%])}n
|
|
316
|
+
OBJ_HEADER = %r{(\d{1,10})[\x00\t\n\f\r ]{1,64}(\d{1,5})[\x00\t\n\f\r ]{0,64}obj(?![^\x00\t\n\f\r ()<>\[\]{}/%])}n
|
|
317
|
+
STREAM = /stream(?:\r\n|\n|\r)?/
|
|
318
|
+
LITERAL_SPECIAL = /[()\\\r]/n
|
|
319
|
+
ESCAPES = { 0x6E => 0x0A, 0x72 => 0x0D, 0x74 => 0x09, 0x62 => 0x08, 0x66 => 0x0C }.freeze
|
|
320
|
+
HEX_PAIR = /#(\h\h)/
|
|
321
|
+
|
|
322
|
+
# Name token (with its slash) -> Symbol, #xx escapes decoded.
|
|
323
|
+
def self.name_symbol(token)
|
|
324
|
+
raw = token.byteslice(1, token.bytesize - 1)
|
|
325
|
+
raw = raw.gsub(HEX_PAIR) { ::Regexp.last_match(1).hex.chr } if raw.include?("#")
|
|
326
|
+
raw.to_sym
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# Moves scanner +s+ (over +data+) past white space and comments;
|
|
330
|
+
# returns the next byte (nil at the end).
|
|
331
|
+
def self.skip_space(s, data)
|
|
332
|
+
while true
|
|
333
|
+
s.skip(SPACE_RUN)
|
|
334
|
+
b = data.getbyte(s.pos)
|
|
335
|
+
if b == 0x25
|
|
336
|
+
s.pos = data.index(LINE_END, s.pos) || data.bytesize
|
|
337
|
+
elsif b.nil? || !SPACE_BYTE[b]
|
|
338
|
+
return b
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
# The token +pattern+ (TOKEN_RUN or NAME_RUN) matches at the
|
|
344
|
+
# scanner position, or nil; the rest of an overlong token is skipped.
|
|
345
|
+
def self.scan_token(s, data, pattern)
|
|
346
|
+
token = s.scan(pattern) or return nil
|
|
347
|
+
b = data.getbyte(s.pos)
|
|
348
|
+
s.pos = data.index(DELIMITER, s.pos) || data.bytesize if b && !DELIMITER_BYTE[b]
|
|
349
|
+
token
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
# A shared count of the elements left to parse (see MAX_ELEMENTS).
|
|
353
|
+
def self.budget
|
|
354
|
+
[MAX_ELEMENTS]
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def initialize(data, budget = Parser.budget)
|
|
358
|
+
@data = data
|
|
359
|
+
@s = StringScanner.new(data)
|
|
360
|
+
@budget = budget
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def pos
|
|
364
|
+
@s.pos
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def pos=(value)
|
|
368
|
+
@s.pos = value.clamp(0, @data.bytesize)
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def skip_space
|
|
372
|
+
Parser.skip_space(@s, @data)
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
# The next object: Integer, Float, true, false, nil, Symbol (name),
|
|
376
|
+
# String, Array, Hash (Symbol keys), Ref, or a Keyword.
|
|
377
|
+
def parse(depth = 0)
|
|
378
|
+
s = @s
|
|
379
|
+
data = @data
|
|
380
|
+
case Parser.skip_space(s, data)
|
|
381
|
+
when nil then raise ParseError, "unexpected end of PDF data"
|
|
382
|
+
when 0x2F then Parser.name_symbol(Parser.scan_token(s, data, NAME_RUN))
|
|
383
|
+
when 0x28 then literal_string
|
|
384
|
+
when 0x3C then data.getbyte(s.pos + 1) == 0x3C ? dictionary(depth) : hex_string
|
|
385
|
+
when 0x5B then array(depth)
|
|
386
|
+
else
|
|
387
|
+
token = Parser.scan_token(s, data, TOKEN_RUN)
|
|
388
|
+
unless token
|
|
389
|
+
s.pos += 1
|
|
390
|
+
raise ParseError, "unexpected delimiter in PDF object"
|
|
391
|
+
end
|
|
392
|
+
atom(token)
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
# [num, gen, value, stream data offset or nil] for the indirect
|
|
397
|
+
# object at +offset+, or nil when there is none.
|
|
398
|
+
def indirect(offset)
|
|
399
|
+
return nil if offset.negative? || offset >= @data.bytesize
|
|
400
|
+
|
|
401
|
+
s = @s
|
|
402
|
+
s.pos = offset
|
|
403
|
+
skip_space
|
|
404
|
+
return nil unless s.scan(OBJ_HEADER)
|
|
405
|
+
|
|
406
|
+
num = s[1].to_i
|
|
407
|
+
gen = s[2].to_i
|
|
408
|
+
value = parse
|
|
409
|
+
value = nil if value.is_a?(Keyword) # "endobj" of an empty object
|
|
410
|
+
start = nil
|
|
411
|
+
if value.is_a?(Hash)
|
|
412
|
+
skip_space
|
|
413
|
+
start = s.pos if s.scan(STREAM)
|
|
414
|
+
end
|
|
415
|
+
[num, gen, value, start]
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
private
|
|
419
|
+
|
|
420
|
+
def atom(token)
|
|
421
|
+
b = token.getbyte(0)
|
|
422
|
+
if b <= 0x39 && (b >= 0x30 || b == 0x2B || b == 0x2D || b == 0x2E)
|
|
423
|
+
return 0 if token.bytesize > 32
|
|
424
|
+
return token.to_f.then { |f| f.finite? ? f : 0.0 } unless INTEGER.match?(token)
|
|
425
|
+
return 0 if token.bytesize > 20
|
|
426
|
+
|
|
427
|
+
value = token.to_i
|
|
428
|
+
return Ref.new(value, @s[1].to_i) if b >= 0x30 && @s.scan(REF_TAIL)
|
|
429
|
+
|
|
430
|
+
return value
|
|
431
|
+
end
|
|
432
|
+
case token
|
|
433
|
+
when "true" then true
|
|
434
|
+
when "false" then false
|
|
435
|
+
when "null" then nil
|
|
436
|
+
else Keyword.new(token)
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def literal_string
|
|
441
|
+
s = @s
|
|
442
|
+
data = @data
|
|
443
|
+
size = data.bytesize
|
|
444
|
+
s.pos += 1
|
|
445
|
+
out = String.new(encoding: Encoding::BINARY)
|
|
446
|
+
depth = 1
|
|
447
|
+
loop do
|
|
448
|
+
pos = s.pos
|
|
449
|
+
stop = data.index(LITERAL_SPECIAL, pos) || size
|
|
450
|
+
out << data.byteslice(pos, stop - pos) if stop > pos
|
|
451
|
+
s.pos = stop
|
|
452
|
+
b = data.getbyte(stop) or break
|
|
453
|
+
s.pos += 1
|
|
454
|
+
case b
|
|
455
|
+
when 0x28
|
|
456
|
+
depth += 1
|
|
457
|
+
out << b
|
|
458
|
+
when 0x29
|
|
459
|
+
depth -= 1
|
|
460
|
+
break if depth.zero?
|
|
461
|
+
|
|
462
|
+
out << b
|
|
463
|
+
when 0x0D
|
|
464
|
+
s.pos += 1 if data.getbyte(s.pos) == 0x0A
|
|
465
|
+
out << 0x0A
|
|
466
|
+
else
|
|
467
|
+
escape(out)
|
|
468
|
+
end
|
|
469
|
+
end
|
|
470
|
+
out
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
def escape(out)
|
|
474
|
+
s = @s
|
|
475
|
+
data = @data
|
|
476
|
+
e = data.getbyte(s.pos) or return
|
|
477
|
+
s.pos += 1
|
|
478
|
+
if (mapped = ESCAPES[e])
|
|
479
|
+
out << mapped
|
|
480
|
+
elsif e >= 0x30 && e <= 0x37
|
|
481
|
+
v = e - 0x30
|
|
482
|
+
2.times do
|
|
483
|
+
d = data.getbyte(s.pos)
|
|
484
|
+
break unless d && d >= 0x30 && d <= 0x37
|
|
485
|
+
|
|
486
|
+
v = (v << 3) | (d - 0x30)
|
|
487
|
+
s.pos += 1
|
|
488
|
+
end
|
|
489
|
+
out << (v & 0xFF)
|
|
490
|
+
elsif e == 0x0D
|
|
491
|
+
s.pos += 1 if data.getbyte(s.pos) == 0x0A
|
|
492
|
+
elsif e != 0x0A
|
|
493
|
+
out << e
|
|
494
|
+
end
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
def hex_string
|
|
498
|
+
s = @s
|
|
499
|
+
data = @data
|
|
500
|
+
start = s.pos + 1
|
|
501
|
+
stop = data.index(">", start) || data.bytesize
|
|
502
|
+
s.pos = stop < data.bytesize ? stop + 1 : stop
|
|
503
|
+
hex = data.byteslice(start, stop - start).delete("^0-9A-Fa-f")
|
|
504
|
+
hex << "0" if hex.bytesize.odd?
|
|
505
|
+
[hex].pack("H*")
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
# Counts one array element or dictionary entry against the budget.
|
|
509
|
+
def count_element
|
|
510
|
+
return if (@budget[0] -= 1) >= 0
|
|
511
|
+
|
|
512
|
+
raise ParseError, "PDF has too many objects (more than #{MAX_ELEMENTS} array elements and dictionary entries)"
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
def array(depth)
|
|
516
|
+
raise ParseError, "PDF objects nested too deeply" if depth > MAX_DEPTH
|
|
517
|
+
|
|
518
|
+
s = @s
|
|
519
|
+
data = @data
|
|
520
|
+
s.pos += 1
|
|
521
|
+
list = []
|
|
522
|
+
loop do
|
|
523
|
+
b = Parser.skip_space(s, data)
|
|
524
|
+
raise ParseError, "unterminated array" unless b
|
|
525
|
+
|
|
526
|
+
if b == 0x5D
|
|
527
|
+
s.pos += 1
|
|
528
|
+
return list
|
|
529
|
+
end
|
|
530
|
+
value = parse(depth + 1)
|
|
531
|
+
raise ParseError, "unexpected #{value.word} in array" if value.is_a?(Keyword)
|
|
532
|
+
|
|
533
|
+
count_element
|
|
534
|
+
list << value
|
|
535
|
+
end
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
def dictionary(depth)
|
|
539
|
+
raise ParseError, "PDF objects nested too deeply" if depth > MAX_DEPTH
|
|
540
|
+
|
|
541
|
+
s = @s
|
|
542
|
+
data = @data
|
|
543
|
+
s.pos += 2
|
|
544
|
+
dict = {}
|
|
545
|
+
loop do
|
|
546
|
+
b = Parser.skip_space(s, data)
|
|
547
|
+
raise ParseError, "unterminated dictionary" unless b
|
|
548
|
+
|
|
549
|
+
if b == 0x3E
|
|
550
|
+
s.pos += data.getbyte(s.pos + 1) == 0x3E ? 2 : 1
|
|
551
|
+
return dict
|
|
552
|
+
end
|
|
553
|
+
raise ParseError, "dictionary key is not a name" unless b == 0x2F
|
|
554
|
+
|
|
555
|
+
key = Parser.name_symbol(Parser.scan_token(s, data, NAME_RUN))
|
|
556
|
+
value = parse(depth + 1)
|
|
557
|
+
raise ParseError, "unexpected #{value.word} in dictionary" if value.is_a?(Keyword)
|
|
558
|
+
|
|
559
|
+
count_element
|
|
560
|
+
dict[key] = value
|
|
561
|
+
end
|
|
562
|
+
end
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
# Cross-reference data, lazy object loading, decryption and the page
|
|
566
|
+
# tree of one document.
|
|
567
|
+
class Document
|
|
568
|
+
include Util
|
|
569
|
+
|
|
570
|
+
MAX_REF_DEPTH = 32
|
|
571
|
+
MISSING = Object.new.freeze
|
|
572
|
+
# (Regexps match bounded lengths only, see Parser.)
|
|
573
|
+
OBJ_SCAN = %r{(?<!\d)(\d{1,10})[\x00\t\n\f\r ]{1,64}(\d{1,5})[\x00\t\n\f\r ]{1,64}obj(?![^\x00\t\n\f\r ()<>\[\]{}/%])}n
|
|
574
|
+
SUBSECTION = /[\x00\t\n\f\r ]{0,64}(\d{1,10})[\x00\t\n\f\r ]{1,64}(\d{1,10})/n
|
|
575
|
+
XREF_ENTRY = /[\x00\t\n\f\r ]{0,64}(\d{1,10})[\x00\t\n\f\r ]{1,64}(\d{1,5})[\x00\t\n\f\r ]{1,64}([nf])/n
|
|
576
|
+
TRAILER_AHEAD = /[\x00\t\n\f\r ]{0,64}trailer/n
|
|
577
|
+
TRAILER_DICT = /trailer[\x00\t\n\f\r ]{0,64}<</n
|
|
578
|
+
ENDSTREAM = /\A[\x00\t\n\f\r ]*endstream/
|
|
579
|
+
NUMBER = /\d{1,20}/n
|
|
580
|
+
TRAILER_KEYS = %i[Root Encrypt ID Info].freeze
|
|
581
|
+
# Distinct objects a document may register (about 80 bytes each).
|
|
582
|
+
MAX_OBJECTS = 2_000_000
|
|
583
|
+
# Cross-reference entries read in all (tables and streams, possibly
|
|
584
|
+
# the same entries again through crafted /Prev chains).
|
|
585
|
+
MAX_XREF_ENTRIES = 4 * MAX_OBJECTS
|
|
586
|
+
# Decoded bytes of all content streams (pages, forms) and of all
|
|
587
|
+
# object streams, at least (and twice the file size for large
|
|
588
|
+
# files): a stream may be a decompression bomb, and many pages may
|
|
589
|
+
# draw the same one. Pages beyond the content budget list their
|
|
590
|
+
# image resources instead.
|
|
591
|
+
MIN_STREAM_BUDGET = 1 << 26
|
|
592
|
+
|
|
593
|
+
attr_reader :data, :trailer
|
|
594
|
+
|
|
595
|
+
def initialize(data)
|
|
596
|
+
@data = data
|
|
597
|
+
@header = data.byteslice(0, 1024).index("%PDF-") or raise ImageError, "not a PDF document (no %PDF- header)"
|
|
598
|
+
@budget = Parser.budget
|
|
599
|
+
@parser = Parser.new(data, @budget)
|
|
600
|
+
@content_left = @object_stream_left = [MIN_STREAM_BUDGET, 2 * data.bytesize].max
|
|
601
|
+
@xref_left = MAX_XREF_ENTRIES
|
|
602
|
+
@object_pairs_left = MAX_OBJECTS
|
|
603
|
+
@offsets = {}
|
|
604
|
+
@cache = {}
|
|
605
|
+
@loading = {}
|
|
606
|
+
@object_streams = {}
|
|
607
|
+
@rebuilt = false
|
|
608
|
+
@security = nil
|
|
609
|
+
@trailer = read_xref
|
|
610
|
+
rebuild unless @trailer && @trailer[:Root]
|
|
611
|
+
setup_security unless @security
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
# Value of indirect object +num+ (nil when missing or broken).
|
|
615
|
+
def object(num)
|
|
616
|
+
return @cache[num] if @cache.key?(num)
|
|
617
|
+
return nil if @loading[num] || @loading.size > MAX_REF_DEPTH
|
|
618
|
+
|
|
619
|
+
@loading[num] = true
|
|
620
|
+
begin
|
|
621
|
+
value = load_object(num)
|
|
622
|
+
value = load_object(num) if value.equal?(MISSING) && rebuild
|
|
623
|
+
value = nil if value.equal?(MISSING)
|
|
624
|
+
ensure
|
|
625
|
+
@loading.delete(num)
|
|
626
|
+
end
|
|
627
|
+
@cache[num] = value
|
|
628
|
+
end
|
|
629
|
+
|
|
630
|
+
# Follows references to a direct value.
|
|
631
|
+
def resolve(value)
|
|
632
|
+
depth = 0
|
|
633
|
+
while value.is_a?(Ref)
|
|
634
|
+
return nil if (depth += 1) > MAX_REF_DEPTH
|
|
635
|
+
|
|
636
|
+
value = object(value.num)
|
|
637
|
+
end
|
|
638
|
+
value
|
|
639
|
+
end
|
|
640
|
+
|
|
641
|
+
# The resolved value if it is a dictionary (a stream's dictionary
|
|
642
|
+
# does not count), else nil.
|
|
643
|
+
def dict_value(value)
|
|
644
|
+
value = resolve(value)
|
|
645
|
+
value.is_a?(Hash) ? value : nil
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
# [filter names, decode parameter dictionaries] of a stream or image
|
|
649
|
+
# dictionary; abbreviated (inline image) names are expanded and the
|
|
650
|
+
# parameter values resolved.
|
|
651
|
+
def filter_chain(dict)
|
|
652
|
+
filters = resolve(dict[:Filter])
|
|
653
|
+
filters = [filters] unless filters.is_a?(Array)
|
|
654
|
+
filters = filters.filter_map do |f|
|
|
655
|
+
f = resolve(f)
|
|
656
|
+
f.is_a?(Symbol) ? Filters::ABBREVIATIONS.fetch(f, f) : nil
|
|
657
|
+
end
|
|
658
|
+
params = resolve(dict[:DecodeParms] || dict[:DP])
|
|
659
|
+
params = [params] unless params.is_a?(Array)
|
|
660
|
+
params = Array.new(filters.size) do |i|
|
|
661
|
+
p = dict_value(params[i])
|
|
662
|
+
p ? p.transform_values { |v| resolve(v) } : {}
|
|
663
|
+
end
|
|
664
|
+
[filters, params]
|
|
665
|
+
end
|
|
666
|
+
|
|
667
|
+
# Raw stream data, decrypted.
|
|
668
|
+
def stream_bytes(stream)
|
|
669
|
+
raw = stream.raw
|
|
670
|
+
return raw unless @security && stream.num
|
|
671
|
+
return raw if resolve(stream.dict[:Type]) == :XRef || identity_crypt?(stream.dict)
|
|
672
|
+
|
|
673
|
+
@security.decrypt_stream(raw, stream.num, stream.gen)
|
|
674
|
+
end
|
|
675
|
+
|
|
676
|
+
# Stream data with all filters applied (no image codecs).
|
|
677
|
+
def decode_stream(stream, limit = STREAM_LIMIT)
|
|
678
|
+
filters, params = filter_chain(stream.dict)
|
|
679
|
+
Filters.decode(stream_bytes(stream), filters, params, limit)
|
|
680
|
+
end
|
|
681
|
+
|
|
682
|
+
# Decoded content stream, or nil when it cannot be decoded (or the
|
|
683
|
+
# document's CONTENT_BUDGET is spent).
|
|
684
|
+
def content(stream)
|
|
685
|
+
return nil if @content_left <= 0
|
|
686
|
+
|
|
687
|
+
data = decode_stream(stream, @content_left < STREAM_LIMIT ? @content_left : STREAM_LIMIT)
|
|
688
|
+
@content_left -= data.bytesize + 1
|
|
689
|
+
data
|
|
690
|
+
rescue Error, *INTERNAL_ERRORS
|
|
691
|
+
nil
|
|
692
|
+
end
|
|
693
|
+
|
|
694
|
+
# The pages in page-tree order; falls back to all page objects (in
|
|
695
|
+
# file order) and then to all image objects for broken documents.
|
|
696
|
+
def pages
|
|
697
|
+
list = walk_pages
|
|
698
|
+
list = walk_pages if list.empty? && rebuild
|
|
699
|
+
list = scan_pages if list.empty?
|
|
700
|
+
list
|
|
701
|
+
end
|
|
702
|
+
|
|
703
|
+
# ImageSources drawn by +page+ in drawing order.
|
|
704
|
+
def page_images(page)
|
|
705
|
+
return [ImageSource.new(page.orphan.dict, page.orphan, nil, {}, nil, nil)] if page.orphan
|
|
706
|
+
|
|
707
|
+
resources = page.resources
|
|
708
|
+
contents = resolve(page.dict[:Contents])
|
|
709
|
+
streams = (contents.is_a?(Array) ? contents.map { |c| resolve(c) } : [contents]).grep(Stream)
|
|
710
|
+
return [] if streams.empty?
|
|
711
|
+
|
|
712
|
+
parts = streams.filter_map { |stream| content(stream) }
|
|
713
|
+
# Undecodable content: list the page's image resources instead.
|
|
714
|
+
return resource_images(resources) if parts.empty?
|
|
715
|
+
|
|
716
|
+
ContentScanner.new(self).images(parts.join("\n"), resources)
|
|
717
|
+
end
|
|
718
|
+
|
|
719
|
+
private
|
|
720
|
+
|
|
721
|
+
def identity_crypt?(dict)
|
|
722
|
+
filters, params = filter_chain(dict)
|
|
723
|
+
i = filters.index(:Crypt) or return false
|
|
724
|
+
(params[i][:Name] || :Identity) == :Identity
|
|
725
|
+
end
|
|
726
|
+
|
|
727
|
+
def resource_images(resources)
|
|
728
|
+
xobjects = dict_value(dict_value(resources)&.[](:XObject)) or return []
|
|
729
|
+
xobjects.filter_map do |name, ref|
|
|
730
|
+
stream = resolve(ref)
|
|
731
|
+
next unless stream.is_a?(Stream) && resolve(stream.dict[:Subtype]) == :Image
|
|
732
|
+
|
|
733
|
+
ImageSource.new(stream.dict, stream, nil, resources, name.to_s, nil)
|
|
734
|
+
end
|
|
735
|
+
end
|
|
736
|
+
|
|
737
|
+
def walk_pages
|
|
738
|
+
root = dict_value(@trailer && @trailer[:Root]) or return []
|
|
739
|
+
list = []
|
|
740
|
+
visited = {}
|
|
741
|
+
stack = [[root[:Pages], nil]]
|
|
742
|
+
until stack.empty?
|
|
743
|
+
ref, inherited = stack.pop
|
|
744
|
+
node = resolve(ref)
|
|
745
|
+
next unless node.is_a?(Hash)
|
|
746
|
+
|
|
747
|
+
id = ref.is_a?(Ref) ? ref.num : node.object_id
|
|
748
|
+
next if visited[id]
|
|
749
|
+
|
|
750
|
+
visited[id] = true
|
|
751
|
+
resources = node.key?(:Resources) ? node[:Resources] : inherited
|
|
752
|
+
kids = resolve(node[:Kids])
|
|
753
|
+
if kids.is_a?(Array) && node[:Type] != :Page
|
|
754
|
+
kids.reverse_each { |kid| stack << [kid, resources] }
|
|
755
|
+
elsif node[:Type] == :Page || !node.key?(:Kids)
|
|
756
|
+
list << Page.new(node, resources, nil)
|
|
757
|
+
end
|
|
758
|
+
end
|
|
759
|
+
list
|
|
760
|
+
end
|
|
761
|
+
|
|
762
|
+
def scan_pages
|
|
763
|
+
rebuild
|
|
764
|
+
pages = []
|
|
765
|
+
images = []
|
|
766
|
+
each_object do |value|
|
|
767
|
+
if value.is_a?(Hash) && value[:Type] == :Page
|
|
768
|
+
pages << Page.new(value, page_resources(value), nil)
|
|
769
|
+
elsif value.is_a?(Stream) && resolve(value.dict[:Subtype]) == :Image
|
|
770
|
+
images << Page.new(nil, nil, value)
|
|
771
|
+
end
|
|
772
|
+
end
|
|
773
|
+
pages.empty? ? images : pages
|
|
774
|
+
end
|
|
775
|
+
|
|
776
|
+
def page_resources(page)
|
|
777
|
+
node = page
|
|
778
|
+
MAX_REF_DEPTH.times do
|
|
779
|
+
return node[:Resources] if node.key?(:Resources)
|
|
780
|
+
|
|
781
|
+
node = dict_value(node[:Parent]) or return nil
|
|
782
|
+
end
|
|
783
|
+
nil
|
|
784
|
+
end
|
|
785
|
+
|
|
786
|
+
# Yields every object, direct ones in file order, then compressed ones.
|
|
787
|
+
def each_object
|
|
788
|
+
direct, compressed = @offsets.partition { |_num, entry| entry.is_a?(Integer) }
|
|
789
|
+
direct.sort_by! { |_num, offset| offset }
|
|
790
|
+
(direct + compressed).each { |num, _entry| yield object(num) }
|
|
791
|
+
end
|
|
792
|
+
|
|
793
|
+
# --- objects -------------------------------------------------------
|
|
794
|
+
|
|
795
|
+
def load_object(num)
|
|
796
|
+
entry = @offsets[num]
|
|
797
|
+
case entry
|
|
798
|
+
when Integer then load_direct(num, entry)
|
|
799
|
+
when Array then load_compressed(num, entry[0], entry[1])
|
|
800
|
+
else MISSING
|
|
801
|
+
end
|
|
802
|
+
end
|
|
803
|
+
|
|
804
|
+
def load_direct(num, offset)
|
|
805
|
+
positions = [offset]
|
|
806
|
+
positions << offset + @header if @header.positive?
|
|
807
|
+
positions.each do |pos|
|
|
808
|
+
parsed = begin
|
|
809
|
+
@parser.indirect(pos)
|
|
810
|
+
rescue ParseError
|
|
811
|
+
nil
|
|
812
|
+
end
|
|
813
|
+
next unless parsed && parsed[0] == num
|
|
814
|
+
|
|
815
|
+
_, gen, value, start = parsed
|
|
816
|
+
value = build_stream(value, start, num, gen) if start
|
|
817
|
+
value = decrypt_strings(value, num, gen) if @security && num != @encrypt_num
|
|
818
|
+
return value
|
|
819
|
+
end
|
|
820
|
+
MISSING
|
|
821
|
+
end
|
|
822
|
+
|
|
823
|
+
def load_compressed(num, stream_num, index)
|
|
824
|
+
container = object_stream(stream_num) or return MISSING
|
|
825
|
+
by_num, pairs, parser, first = container
|
|
826
|
+
pair = pairs[index]
|
|
827
|
+
offset = pair && pair[0] == num ? pair[1] : by_num[num]
|
|
828
|
+
return MISSING unless offset
|
|
829
|
+
|
|
830
|
+
parser.pos = first + offset
|
|
831
|
+
value = parser.parse
|
|
832
|
+
value.is_a?(Keyword) ? nil : value
|
|
833
|
+
rescue ParseError
|
|
834
|
+
MISSING
|
|
835
|
+
end
|
|
836
|
+
|
|
837
|
+
# [{num => offset}, [[num, offset], ...], parser, first] of object
|
|
838
|
+
# stream +num+, or nil.
|
|
839
|
+
def object_stream(num)
|
|
840
|
+
return @object_streams[num] if @object_streams.key?(num)
|
|
841
|
+
|
|
842
|
+
@object_streams[num] = nil
|
|
843
|
+
stream = object(num)
|
|
844
|
+
return nil unless stream.is_a?(Stream)
|
|
845
|
+
|
|
846
|
+
first = int(resolve(stream.dict[:First])) or return nil
|
|
847
|
+
return nil if @object_stream_left <= 0
|
|
848
|
+
|
|
849
|
+
data = decode_stream(stream, @object_stream_left < STREAM_LIMIT ? @object_stream_left : STREAM_LIMIT)
|
|
850
|
+
@object_stream_left -= data.bytesize + 1
|
|
851
|
+
return nil if first.negative? || first > data.bytesize
|
|
852
|
+
|
|
853
|
+
# At most N pairs, and MAX_OBJECTS pairs in all object streams.
|
|
854
|
+
count = (int(resolve(stream.dict[:N])) || 0).clamp(0, @object_pairs_left)
|
|
855
|
+
numbers = []
|
|
856
|
+
header = StringScanner.new(data.byteslice(0, first))
|
|
857
|
+
numbers << header.matched.to_i while numbers.size < 2 * count && header.skip_until(NUMBER)
|
|
858
|
+
count = numbers.size / 2
|
|
859
|
+
@object_pairs_left -= count
|
|
860
|
+
pairs = Array.new(count) { |i| [numbers[2 * i], numbers[2 * i + 1]] }
|
|
861
|
+
by_num = {}
|
|
862
|
+
pairs.each { |n, offset| by_num[n] ||= offset }
|
|
863
|
+
@object_streams[num] = [by_num, pairs, Parser.new(data, @budget), first]
|
|
864
|
+
rescue Error, *INTERNAL_ERRORS
|
|
865
|
+
nil
|
|
866
|
+
end
|
|
867
|
+
|
|
868
|
+
def build_stream(dict, start, num, gen, resolve_length: true)
|
|
869
|
+
length = dict[:Length]
|
|
870
|
+
length = resolve(length) if resolve_length && length.is_a?(Ref)
|
|
871
|
+
size = @data.bytesize
|
|
872
|
+
if length.is_a?(Integer) && length >= 0 && start + length <= size &&
|
|
873
|
+
(start + length == size || ENDSTREAM.match?(@data.byteslice(start + length, 64)))
|
|
874
|
+
raw = @data.byteslice(start, length)
|
|
875
|
+
else
|
|
876
|
+
stop = @data.index("endstream", start) || size
|
|
877
|
+
stop -= 1 if stop > start && @data.getbyte(stop - 1) == 0x0A
|
|
878
|
+
stop -= 1 if stop > start && @data.getbyte(stop - 1) == 0x0D
|
|
879
|
+
raw = @data.byteslice(start, stop - start)
|
|
880
|
+
end
|
|
881
|
+
Stream.new(dict, raw, num, gen)
|
|
882
|
+
end
|
|
883
|
+
|
|
884
|
+
def decrypt_strings(value, num, gen)
|
|
885
|
+
case value
|
|
886
|
+
when String then @security.decrypt_string(value, num, gen)
|
|
887
|
+
when Array then value.map! { |v| decrypt_strings(v, num, gen) }
|
|
888
|
+
when Hash then value.transform_values! { |v| decrypt_strings(v, num, gen) }
|
|
889
|
+
when Stream
|
|
890
|
+
decrypt_strings(value.dict, num, gen)
|
|
891
|
+
value
|
|
892
|
+
else value
|
|
893
|
+
end
|
|
894
|
+
end
|
|
895
|
+
|
|
896
|
+
# --- cross-reference data -----------------------------------------
|
|
897
|
+
|
|
898
|
+
# Merged trailer of all cross-reference sections (newest wins).
|
|
899
|
+
def read_xref
|
|
900
|
+
offset = startxref
|
|
901
|
+
trailers = []
|
|
902
|
+
seen = {}
|
|
903
|
+
while offset.is_a?(Integer) && !seen[offset] && seen.size < 256
|
|
904
|
+
seen[offset] = true
|
|
905
|
+
trailer = read_section(offset) or break
|
|
906
|
+
trailers << trailer
|
|
907
|
+
stream = trailer[:XRefStm]
|
|
908
|
+
if stream.is_a?(Integer) && !seen[stream]
|
|
909
|
+
seen[stream] = true
|
|
910
|
+
read_section(stream)
|
|
911
|
+
end
|
|
912
|
+
offset = trailer[:Prev]
|
|
913
|
+
end
|
|
914
|
+
trailers.reverse.inject { |older, newer| older.merge(newer) }
|
|
915
|
+
end
|
|
916
|
+
|
|
917
|
+
def startxref
|
|
918
|
+
from = [@data.bytesize - 4096, 0].max
|
|
919
|
+
tail = @data.byteslice(from, @data.bytesize - from)
|
|
920
|
+
i = tail.rindex("startxref") or return nil
|
|
921
|
+
tail.byteslice(i + 9, 40)[/\A[\x00\t\n\f\r ]*(\d{1,12})/, 1]&.to_i
|
|
922
|
+
end
|
|
923
|
+
|
|
924
|
+
# Reads the section at +offset+ (or at +offset+ counted from the
|
|
925
|
+
# header); returns its trailer dictionary or nil.
|
|
926
|
+
def read_section(offset)
|
|
927
|
+
positions = [offset]
|
|
928
|
+
positions << offset + @header if @header.positive?
|
|
929
|
+
positions.each do |pos|
|
|
930
|
+
next unless pos.between?(0, @data.bytesize - 1)
|
|
931
|
+
|
|
932
|
+
scanner = StringScanner.new(@data)
|
|
933
|
+
scanner.pos = pos
|
|
934
|
+
Parser.skip_space(scanner, @data)
|
|
935
|
+
trailer = begin
|
|
936
|
+
if scanner.scan(/xref/)
|
|
937
|
+
read_table(scanner)
|
|
938
|
+
else
|
|
939
|
+
read_xref_stream(scanner.pos)
|
|
940
|
+
end
|
|
941
|
+
rescue ParseError
|
|
942
|
+
nil
|
|
943
|
+
end
|
|
944
|
+
return trailer if trailer
|
|
945
|
+
end
|
|
946
|
+
nil
|
|
947
|
+
end
|
|
948
|
+
|
|
949
|
+
def read_table(s)
|
|
950
|
+
until s.match?(TRAILER_AHEAD)
|
|
951
|
+
break unless s.scan(SUBSECTION)
|
|
952
|
+
|
|
953
|
+
start = s[1].to_i
|
|
954
|
+
count = s[2].to_i
|
|
955
|
+
i = 0
|
|
956
|
+
while i < count && (@xref_left -= 1) >= 0 && s.scan(XREF_ENTRY)
|
|
957
|
+
offset = s[1].to_i
|
|
958
|
+
if s[3] == "n"
|
|
959
|
+
register(start + i, offset) if offset.positive?
|
|
960
|
+
elsif i.zero? && start == 1 && s[2] == "65535"
|
|
961
|
+
start = 0 # a common off-by-one: the table really starts at object 0
|
|
962
|
+
end
|
|
963
|
+
i += 1
|
|
964
|
+
end
|
|
965
|
+
end
|
|
966
|
+
s.skip_until(/trailer/) or return nil
|
|
967
|
+
@parser.pos = s.pos
|
|
968
|
+
trailer = @parser.parse
|
|
969
|
+
trailer.is_a?(Hash) ? trailer : nil
|
|
970
|
+
end
|
|
971
|
+
|
|
972
|
+
def read_xref_stream(pos)
|
|
973
|
+
num, gen, dict, start = @parser.indirect(pos)
|
|
974
|
+
return nil unless start && dict.is_a?(Hash) && (dict[:Type] == :XRef || dict.key?(:W))
|
|
975
|
+
|
|
976
|
+
widths = dict[:W]
|
|
977
|
+
return nil unless widths.is_a?(Array) && widths.size >= 3 &&
|
|
978
|
+
widths.first(3).all? { |w| w.is_a?(Integer) && w.between?(0, 8) }
|
|
979
|
+
|
|
980
|
+
w0, w1, w2 = widths
|
|
981
|
+
row = w0 + w1 + w2
|
|
982
|
+
return dict if row.zero?
|
|
983
|
+
|
|
984
|
+
index = dict[:Index].is_a?(Array) ? dict[:Index] : [0, int(dict[:Size]) || 0]
|
|
985
|
+
rows = 0
|
|
986
|
+
index.each_slice(2) { |_, count| rows += count if count.is_a?(Integer) && count.positive? }
|
|
987
|
+
rows = @xref_left if rows > @xref_left
|
|
988
|
+
return dict if rows <= 0
|
|
989
|
+
|
|
990
|
+
stream = build_stream(dict, start, num, gen, resolve_length: false)
|
|
991
|
+
filters, params = filter_chain(dict)
|
|
992
|
+
data = Filters.decode(stream.raw, filters, params, rows * row)
|
|
993
|
+
@xref_left -= data.bytesize / row
|
|
994
|
+
pos = 0
|
|
995
|
+
index.each_slice(2) do |first, count|
|
|
996
|
+
break unless first.is_a?(Integer) && count.is_a?(Integer) && first >= 0
|
|
997
|
+
|
|
998
|
+
pos = read_xref_rows(data, pos, first, count, w0, w1, w2)
|
|
999
|
+
end
|
|
1000
|
+
dict
|
|
1001
|
+
rescue Error, *INTERNAL_ERRORS
|
|
1002
|
+
nil
|
|
1003
|
+
end
|
|
1004
|
+
|
|
1005
|
+
def read_xref_rows(data, pos, first, count, w0, w1, w2)
|
|
1006
|
+
row = w0 + w1 + w2
|
|
1007
|
+
i = 0
|
|
1008
|
+
while i < count && pos + row <= data.bytesize
|
|
1009
|
+
type = w0.zero? ? 1 : be_int(data, pos, w0)
|
|
1010
|
+
field = be_int(data, pos + w0, w1)
|
|
1011
|
+
if type == 1
|
|
1012
|
+
register(first + i, field) if field.positive?
|
|
1013
|
+
elsif type == 2
|
|
1014
|
+
register(first + i, [field, be_int(data, pos + w0 + w1, w2)])
|
|
1015
|
+
end
|
|
1016
|
+
pos += row
|
|
1017
|
+
i += 1
|
|
1018
|
+
end
|
|
1019
|
+
pos
|
|
1020
|
+
end
|
|
1021
|
+
|
|
1022
|
+
# Records where object +num+ is (the first entry found wins), up to
|
|
1023
|
+
# MAX_OBJECTS objects.
|
|
1024
|
+
def register(num, entry)
|
|
1025
|
+
@offsets[num] ||= entry if @offsets.size < MAX_OBJECTS || @offsets.key?(num)
|
|
1026
|
+
end
|
|
1027
|
+
|
|
1028
|
+
# Rebuilds the cross-reference data by scanning the whole file.
|
|
1029
|
+
# Returns true if it ran (only the first call does).
|
|
1030
|
+
def rebuild
|
|
1031
|
+
return false if @rebuilt
|
|
1032
|
+
|
|
1033
|
+
@rebuilt = true
|
|
1034
|
+
offsets = {}
|
|
1035
|
+
scan_objects { |num, offset| offsets[num] = offset if offsets.size < MAX_OBJECTS || offsets.key?(num) }
|
|
1036
|
+
@offsets = offsets
|
|
1037
|
+
@object_streams.clear
|
|
1038
|
+
@cache.clear
|
|
1039
|
+
scanned = scanned_trailer
|
|
1040
|
+
@trailer = (@trailer || {}).merge(scanned) { |_key, old, _new| old }
|
|
1041
|
+
setup_security if @security.nil? && !@securing && @trailer[:Encrypt]
|
|
1042
|
+
register_object_streams
|
|
1043
|
+
unless dict_value(@trailer[:Root])
|
|
1044
|
+
@trailer[:Root] = dict_value(scanned[:Root]) ? scanned[:Root] : find_catalog
|
|
1045
|
+
end
|
|
1046
|
+
true
|
|
1047
|
+
end
|
|
1048
|
+
|
|
1049
|
+
# Yields [num, offset] of every "N G obj", skipping stream data. The
|
|
1050
|
+
# positions of the next keywords are cached so that the scan stays
|
|
1051
|
+
# linear in the file size.
|
|
1052
|
+
def scan_objects
|
|
1053
|
+
data = @data
|
|
1054
|
+
size = data.bytesize
|
|
1055
|
+
pos = 0
|
|
1056
|
+
stream = endobj = other = -1
|
|
1057
|
+
while (m = OBJ_SCAN.match(data, pos))
|
|
1058
|
+
yield m[1].to_i, m.begin(0)
|
|
1059
|
+
pos = m.end(0)
|
|
1060
|
+
stream = data.index("stream", pos) || size if stream < pos
|
|
1061
|
+
endobj = data.index("endobj", pos) || size if endobj < pos
|
|
1062
|
+
other = data.index("obj", pos) || size if other < pos
|
|
1063
|
+
next if stream == size || endobj < stream || other < stream
|
|
1064
|
+
|
|
1065
|
+
stop = data.index("endstream", stream + 6) or next
|
|
1066
|
+
pos = stop + 9
|
|
1067
|
+
end
|
|
1068
|
+
end
|
|
1069
|
+
|
|
1070
|
+
# Registers the objects of all object streams (direct objects win).
|
|
1071
|
+
def register_object_streams
|
|
1072
|
+
objects_containing("/ObjStm").each do |num|
|
|
1073
|
+
stream = object(num)
|
|
1074
|
+
next unless stream.is_a?(Stream) && resolve(stream.dict[:Type]) == :ObjStm
|
|
1075
|
+
|
|
1076
|
+
container = object_stream(num) or next
|
|
1077
|
+
container[1].each_with_index do |(inner, _offset), index|
|
|
1078
|
+
next if @offsets[inner].is_a?(Integer) || (@offsets.size >= MAX_OBJECTS && !@offsets.key?(inner))
|
|
1079
|
+
|
|
1080
|
+
@offsets[inner] = [num, index]
|
|
1081
|
+
end
|
|
1082
|
+
end
|
|
1083
|
+
end
|
|
1084
|
+
|
|
1085
|
+
# Numbers of the direct objects whose text contains +needle+.
|
|
1086
|
+
def objects_containing(needle)
|
|
1087
|
+
sorted = @offsets.select { |_num, entry| entry.is_a?(Integer) }.sort_by { |_num, offset| offset }
|
|
1088
|
+
starts = sorted.map(&:last)
|
|
1089
|
+
found = []
|
|
1090
|
+
pos = 0
|
|
1091
|
+
while (pos = @data.index(needle, pos))
|
|
1092
|
+
i = starts.bsearch_index { |offset| offset > pos } || starts.size
|
|
1093
|
+
found << sorted[i - 1][0] if i.positive?
|
|
1094
|
+
pos += needle.bytesize
|
|
1095
|
+
end
|
|
1096
|
+
found.uniq
|
|
1097
|
+
end
|
|
1098
|
+
|
|
1099
|
+
# Trailer entries from "trailer" dictionaries and cross-reference
|
|
1100
|
+
# stream dictionaries anywhere in the file (later ones win).
|
|
1101
|
+
def scanned_trailer
|
|
1102
|
+
trailer = {}
|
|
1103
|
+
@data.scan(TRAILER_DICT) do
|
|
1104
|
+
@parser.pos = ::Regexp.last_match.end(0) - 2
|
|
1105
|
+
dict = begin
|
|
1106
|
+
@parser.parse
|
|
1107
|
+
rescue ParseError
|
|
1108
|
+
nil
|
|
1109
|
+
end
|
|
1110
|
+
trailer.merge!(dict.slice(*TRAILER_KEYS)) if dict.is_a?(Hash)
|
|
1111
|
+
end
|
|
1112
|
+
objects_containing("/XRef").each do |num|
|
|
1113
|
+
_, _, dict, = @parser.indirect(@offsets[num])
|
|
1114
|
+
trailer.merge!(dict.slice(*TRAILER_KEYS)) if dict.is_a?(Hash) && dict[:Type] == :XRef
|
|
1115
|
+
rescue ParseError
|
|
1116
|
+
next
|
|
1117
|
+
end
|
|
1118
|
+
trailer
|
|
1119
|
+
end
|
|
1120
|
+
|
|
1121
|
+
def find_catalog
|
|
1122
|
+
candidates = objects_containing("/Catalog") + @offsets.select { |_num, entry| entry.is_a?(Array) }.keys
|
|
1123
|
+
num = candidates.find { |n| dict_value(Ref.new(n, 0))&.[](:Type) == :Catalog }
|
|
1124
|
+
num && Ref.new(num, 0)
|
|
1125
|
+
end
|
|
1126
|
+
|
|
1127
|
+
# --- encryption ------------------------------------------------------
|
|
1128
|
+
|
|
1129
|
+
def setup_security
|
|
1130
|
+
encrypt = @trailer && @trailer[:Encrypt]
|
|
1131
|
+
return unless encrypt
|
|
1132
|
+
|
|
1133
|
+
@securing = true
|
|
1134
|
+
@encrypt_num = encrypt.num if encrypt.is_a?(Ref)
|
|
1135
|
+
dict = dict_value(encrypt) or raise UnsupportedImageError, "encrypted PDF with an invalid /Encrypt dictionary"
|
|
1136
|
+
ids = resolve(@trailer[:ID])
|
|
1137
|
+
id = ids.is_a?(Array) ? resolve(ids[0]) : nil
|
|
1138
|
+
@security = Security.new(dict, id.is_a?(String) ? id : "".b, self)
|
|
1139
|
+
@cache.clear # objects loaded so far were not decrypted
|
|
1140
|
+
@object_streams.clear
|
|
1141
|
+
ensure
|
|
1142
|
+
@securing = false
|
|
1143
|
+
end
|
|
1144
|
+
end
|
|
1145
|
+
|
|
1146
|
+
# Standard security handler with an empty user password (ISO 32000-1
|
|
1147
|
+
# 7.6.3, ISO 32000-2 7.6.4): RC4 40-128 bit, AES-128 and AES-256.
|
|
1148
|
+
class Security
|
|
1149
|
+
include Util
|
|
1150
|
+
|
|
1151
|
+
PADDING = [
|
|
1152
|
+
0x28, 0xBF, 0x4E, 0x5E, 0x4E, 0x75, 0x8A, 0x41, 0x64, 0x00, 0x4E, 0x56, 0xFF, 0xFA, 0x01, 0x08,
|
|
1153
|
+
0x2E, 0x2E, 0x00, 0xB6, 0xD0, 0x68, 0x3E, 0x80, 0x2F, 0x0C, 0xA9, 0xFE, 0x64, 0x53, 0x69, 0x7A
|
|
1154
|
+
].pack("C*").freeze
|
|
1155
|
+
PASSWORD = "password-protected PDF (only documents with an empty user password can be decrypted)"
|
|
1156
|
+
|
|
1157
|
+
def initialize(dict, id, document)
|
|
1158
|
+
begin
|
|
1159
|
+
require "digest"
|
|
1160
|
+
require "openssl"
|
|
1161
|
+
rescue LoadError
|
|
1162
|
+
raise UnsupportedImageError, "encrypted PDF (needs the openssl library)"
|
|
1163
|
+
end
|
|
1164
|
+
@doc = document
|
|
1165
|
+
filter = document.resolve(dict[:Filter])
|
|
1166
|
+
raise UnsupportedImageError, "encrypted PDF (security handler #{filter})" unless filter == :Standard
|
|
1167
|
+
|
|
1168
|
+
@v = int(document.resolve(dict[:V])) || 0
|
|
1169
|
+
@r = int(document.resolve(dict[:R])) || 2
|
|
1170
|
+
case @v
|
|
1171
|
+
when 1, 2 then setup_rc4(dict, id, @v == 1 ? 40 : int(document.resolve(dict[:Length])) || 40)
|
|
1172
|
+
when 4 then setup_crypt_filters(dict, id)
|
|
1173
|
+
when 5 then setup_aes256(dict)
|
|
1174
|
+
else raise UnsupportedImageError, "encrypted PDF (unsupported encryption version #{@v})"
|
|
1175
|
+
end
|
|
1176
|
+
rescue Error
|
|
1177
|
+
raise
|
|
1178
|
+
rescue StandardError => e # OpenSSL errors for malformed keys
|
|
1179
|
+
raise UnsupportedImageError, "encrypted PDF (#{e.class}: #{e.message})"
|
|
1180
|
+
end
|
|
1181
|
+
|
|
1182
|
+
def decrypt_string(data, num, gen)
|
|
1183
|
+
decrypt(@string_method, data, num, gen)
|
|
1184
|
+
end
|
|
1185
|
+
|
|
1186
|
+
def decrypt_stream(data, num, gen)
|
|
1187
|
+
decrypt(@stream_method, data, num, gen)
|
|
1188
|
+
end
|
|
1189
|
+
|
|
1190
|
+
private
|
|
1191
|
+
|
|
1192
|
+
def setup_rc4(dict, id, bits)
|
|
1193
|
+
@key = file_key(dict, id, (bits / 8).clamp(5, 16))
|
|
1194
|
+
@stream_method = @string_method = :rc4
|
|
1195
|
+
end
|
|
1196
|
+
|
|
1197
|
+
def setup_crypt_filters(dict, id)
|
|
1198
|
+
filters = @doc.dict_value(dict[:CF]) || {}
|
|
1199
|
+
methods = %i[StmF StrF].map do |key|
|
|
1200
|
+
name = @doc.resolve(dict[key]) || :Identity
|
|
1201
|
+
next :identity if name == :Identity
|
|
1202
|
+
|
|
1203
|
+
cf = @doc.dict_value(filters[name]) or raise UnsupportedImageError, "encrypted PDF (crypt filter #{name})"
|
|
1204
|
+
case @doc.resolve(cf[:CFM])
|
|
1205
|
+
when :V2 then :rc4
|
|
1206
|
+
when :AESV2 then :aes128
|
|
1207
|
+
when :None, nil then :identity
|
|
1208
|
+
else raise UnsupportedImageError, "encrypted PDF (crypt filter method #{cf[:CFM]})"
|
|
1209
|
+
end
|
|
1210
|
+
end
|
|
1211
|
+
@stream_method, @string_method = methods
|
|
1212
|
+
bits = int(@doc.resolve(dict[:Length])) || 128
|
|
1213
|
+
bits *= 8 if bits <= 16 # some writers give bytes
|
|
1214
|
+
bits = 128 if methods.include?(:aes128)
|
|
1215
|
+
@key = file_key(dict, id, (bits / 8).clamp(5, 16))
|
|
1216
|
+
end
|
|
1217
|
+
|
|
1218
|
+
# Algorithms 2 and 4/5: file key from the empty user password,
|
|
1219
|
+
# checked against /U.
|
|
1220
|
+
def file_key(dict, id, length)
|
|
1221
|
+
o = string(dict[:O])
|
|
1222
|
+
u = string(dict[:U])
|
|
1223
|
+
p = int(@doc.resolve(dict[:P])) || 0
|
|
1224
|
+
md5 = Digest::MD5.new
|
|
1225
|
+
md5 << PADDING << o.byteslice(0, 32) << [p & 0xFFFFFFFF].pack("V") << id
|
|
1226
|
+
md5 << "\xFF\xFF\xFF\xFF".b if @r >= 4 && @doc.resolve(dict[:EncryptMetadata]) == false
|
|
1227
|
+
key = md5.digest
|
|
1228
|
+
length = 5 if @r == 2
|
|
1229
|
+
50.times { key = Digest::MD5.digest(key.byteslice(0, length)) } if @r >= 3
|
|
1230
|
+
key = key.byteslice(0, length)
|
|
1231
|
+
valid = if @r == 2
|
|
1232
|
+
rc4(key, PADDING) == u.byteslice(0, 32)
|
|
1233
|
+
else
|
|
1234
|
+
x = rc4(key, Digest::MD5.digest(PADDING + id))
|
|
1235
|
+
1.upto(19) { |i| x = rc4(key.bytes.map { |b| b ^ i }.pack("C*"), x) }
|
|
1236
|
+
x == u.byteslice(0, 16)
|
|
1237
|
+
end
|
|
1238
|
+
raise UnsupportedImageError, PASSWORD unless valid
|
|
1239
|
+
|
|
1240
|
+
key
|
|
1241
|
+
end
|
|
1242
|
+
|
|
1243
|
+
def setup_aes256(dict)
|
|
1244
|
+
u = string(dict[:U])
|
|
1245
|
+
ue = string(dict[:UE])
|
|
1246
|
+
raise UnsupportedImageError, "encrypted PDF (invalid /U or /UE)" if u.bytesize < 48 || ue.bytesize < 32
|
|
1247
|
+
|
|
1248
|
+
empty = "".b
|
|
1249
|
+
check = @r >= 6 ? hash_r6(empty, u.byteslice(32, 8)) : Digest::SHA256.digest(u.byteslice(32, 8))
|
|
1250
|
+
raise UnsupportedImageError, PASSWORD unless check == u.byteslice(0, 32)
|
|
1251
|
+
|
|
1252
|
+
intermediate = @r >= 6 ? hash_r6(empty, u.byteslice(40, 8)) : Digest::SHA256.digest(u.byteslice(40, 8))
|
|
1253
|
+
cipher = OpenSSL::Cipher.new("AES-256-CBC").decrypt
|
|
1254
|
+
cipher.key = intermediate
|
|
1255
|
+
cipher.iv = "\0".b * 16
|
|
1256
|
+
cipher.padding = 0
|
|
1257
|
+
@key = cipher.update(ue.byteslice(0, 32)) + cipher.final
|
|
1258
|
+
filters = @doc.dict_value(dict[:CF]) || {}
|
|
1259
|
+
@stream_method, @string_method = %i[StmF StrF].map do |key|
|
|
1260
|
+
name = @doc.resolve(dict[key]) || :Identity
|
|
1261
|
+
cf = @doc.dict_value(filters[name])
|
|
1262
|
+
name == :Identity || (cf && @doc.resolve(cf[:CFM]) == :None) ? :identity : :aes256
|
|
1263
|
+
end
|
|
1264
|
+
end
|
|
1265
|
+
|
|
1266
|
+
# ISO 32000-2 Algorithm 2.B (empty password, no user key data).
|
|
1267
|
+
def hash_r6(password, salt)
|
|
1268
|
+
k = Digest::SHA256.digest(password + salt)
|
|
1269
|
+
i = 0
|
|
1270
|
+
loop do
|
|
1271
|
+
k1 = (password + k) * 64
|
|
1272
|
+
cipher = OpenSSL::Cipher.new("AES-128-CBC").encrypt
|
|
1273
|
+
cipher.key = k.byteslice(0, 16)
|
|
1274
|
+
cipher.iv = k.byteslice(16, 16)
|
|
1275
|
+
cipher.padding = 0
|
|
1276
|
+
e = cipher.update(k1) + cipher.final
|
|
1277
|
+
k = case e.byteslice(0, 16).sum % 3
|
|
1278
|
+
when 0 then Digest::SHA256.digest(e)
|
|
1279
|
+
when 1 then Digest::SHA384.digest(e)
|
|
1280
|
+
else Digest::SHA512.digest(e)
|
|
1281
|
+
end
|
|
1282
|
+
i += 1
|
|
1283
|
+
break if i >= 64 && e.getbyte(-1) <= i - 32
|
|
1284
|
+
end
|
|
1285
|
+
k.byteslice(0, 32)
|
|
1286
|
+
end
|
|
1287
|
+
|
|
1288
|
+
def string(value)
|
|
1289
|
+
value = @doc.resolve(value)
|
|
1290
|
+
value.is_a?(String) ? value : "".b
|
|
1291
|
+
end
|
|
1292
|
+
|
|
1293
|
+
def decrypt(method, data, num, gen)
|
|
1294
|
+
case method
|
|
1295
|
+
when :rc4 then rc4(object_key(num, gen, false), data)
|
|
1296
|
+
when :aes128 then aes_cbc("AES-128-CBC", object_key(num, gen, true), data)
|
|
1297
|
+
when :aes256 then aes_cbc("AES-256-CBC", @key, data)
|
|
1298
|
+
else data
|
|
1299
|
+
end
|
|
1300
|
+
rescue OpenSSL::Cipher::CipherError => e
|
|
1301
|
+
raise ImageError, "cannot decrypt PDF data (#{e.message})"
|
|
1302
|
+
end
|
|
1303
|
+
|
|
1304
|
+
def object_key(num, gen, aes)
|
|
1305
|
+
material = @key + [num].pack("V").byteslice(0, 3) + [gen].pack("V").byteslice(0, 2)
|
|
1306
|
+
material << "sAlT" if aes
|
|
1307
|
+
Digest::MD5.digest(material).byteslice(0, [@key.bytesize + 5, 16].min)
|
|
1308
|
+
end
|
|
1309
|
+
|
|
1310
|
+
def aes_cbc(name, key, data)
|
|
1311
|
+
return "".b if data.bytesize < 32
|
|
1312
|
+
|
|
1313
|
+
cipher = OpenSSL::Cipher.new(name).decrypt
|
|
1314
|
+
cipher.key = key
|
|
1315
|
+
cipher.iv = data.byteslice(0, 16)
|
|
1316
|
+
cipher.padding = 0
|
|
1317
|
+
out = cipher.update(data.byteslice(16, (data.bytesize - 16) & ~15)) + cipher.final
|
|
1318
|
+
pad = out.getbyte(-1) || 0
|
|
1319
|
+
if pad.between?(1, 16) && out.byteslice(-pad, pad).bytes.all?(pad)
|
|
1320
|
+
out.byteslice(0, out.bytesize - pad)
|
|
1321
|
+
else
|
|
1322
|
+
out
|
|
1323
|
+
end
|
|
1324
|
+
end
|
|
1325
|
+
|
|
1326
|
+
def rc4(key, data)
|
|
1327
|
+
@openssl_rc4 = openssl_rc4? if @openssl_rc4.nil?
|
|
1328
|
+
return RC4.new(key).process(data) unless @openssl_rc4
|
|
1329
|
+
|
|
1330
|
+
cipher = OpenSSL::Cipher.new("RC4").decrypt
|
|
1331
|
+
cipher.key_len = key.bytesize
|
|
1332
|
+
cipher.key = key
|
|
1333
|
+
cipher.update(data) + cipher.final
|
|
1334
|
+
end
|
|
1335
|
+
|
|
1336
|
+
# OpenSSL 3 only offers RC4 with its legacy provider loaded.
|
|
1337
|
+
def openssl_rc4?
|
|
1338
|
+
OpenSSL::Cipher.new("RC4")
|
|
1339
|
+
true
|
|
1340
|
+
rescue OpenSSL::Cipher::CipherError, RuntimeError
|
|
1341
|
+
false
|
|
1342
|
+
end
|
|
1343
|
+
|
|
1344
|
+
# Pure-Ruby RC4 (processed in chunks so that YJIT compiles the loop).
|
|
1345
|
+
class RC4
|
|
1346
|
+
def initialize(key)
|
|
1347
|
+
s = (0..255).to_a
|
|
1348
|
+
j = 0
|
|
1349
|
+
n = key.bytesize
|
|
1350
|
+
256.times do |i|
|
|
1351
|
+
j = (j + s[i] + key.getbyte(i % n)) & 255
|
|
1352
|
+
s[i], s[j] = s[j], s[i]
|
|
1353
|
+
end
|
|
1354
|
+
@s = s
|
|
1355
|
+
@i = 0
|
|
1356
|
+
@j = 0
|
|
1357
|
+
end
|
|
1358
|
+
|
|
1359
|
+
def process(data)
|
|
1360
|
+
n = data.bytesize
|
|
1361
|
+
out = "\0".b * n
|
|
1362
|
+
pos = 0
|
|
1363
|
+
while pos < n
|
|
1364
|
+
stop = [pos + 4096, n].min
|
|
1365
|
+
chunk(data, out, pos, stop)
|
|
1366
|
+
pos = stop
|
|
1367
|
+
end
|
|
1368
|
+
out
|
|
1369
|
+
end
|
|
1370
|
+
|
|
1371
|
+
private
|
|
1372
|
+
|
|
1373
|
+
def chunk(data, out, k, stop)
|
|
1374
|
+
s = @s
|
|
1375
|
+
i = @i
|
|
1376
|
+
j = @j
|
|
1377
|
+
while k < stop
|
|
1378
|
+
i = (i + 1) & 255
|
|
1379
|
+
si = s[i]
|
|
1380
|
+
j = (j + si) & 255
|
|
1381
|
+
sj = s[j]
|
|
1382
|
+
s[i] = sj
|
|
1383
|
+
s[j] = si
|
|
1384
|
+
out.setbyte(k, data.getbyte(k) ^ s[(si + sj) & 255])
|
|
1385
|
+
k += 1
|
|
1386
|
+
end
|
|
1387
|
+
@i = i
|
|
1388
|
+
@j = j
|
|
1389
|
+
end
|
|
1390
|
+
end
|
|
1391
|
+
end
|
|
1392
|
+
|
|
1393
|
+
# Stream filters (ISO 32000-1 7.4) other than the image codecs.
|
|
1394
|
+
module Filters
|
|
1395
|
+
extend Util
|
|
1396
|
+
|
|
1397
|
+
ABBREVIATIONS = {
|
|
1398
|
+
AHx: :ASCIIHexDecode, A85: :ASCII85Decode, LZW: :LZWDecode, Fl: :FlateDecode,
|
|
1399
|
+
RL: :RunLengthDecode, CCF: :CCITTFaxDecode, DCT: :DCTDecode
|
|
1400
|
+
}.freeze
|
|
1401
|
+
NONZERO = /[^\x00]/n
|
|
1402
|
+
HEX_DIGITS = "^0-9A-Fa-f"
|
|
1403
|
+
WHITESPACE = "\x00\t\n\f\r "
|
|
1404
|
+
|
|
1405
|
+
class << self
|
|
1406
|
+
# Applies +filters+ in order; the output of the last one is cut
|
|
1407
|
+
# at +limit+ bytes. (Intermediate results are encoded data of the
|
|
1408
|
+
# later filters, which never needs much more room than the result.)
|
|
1409
|
+
def decode(data, filters, params, limit)
|
|
1410
|
+
last = filters.size - 1
|
|
1411
|
+
filters.each_with_index do |filter, i|
|
|
1412
|
+
data = apply(data, filter, params[i] || {}, i == last ? limit : 2 * limit + 4096)
|
|
1413
|
+
end
|
|
1414
|
+
data.bytesize > limit ? data.byteslice(0, limit) : data
|
|
1415
|
+
end
|
|
1416
|
+
|
|
1417
|
+
private
|
|
1418
|
+
|
|
1419
|
+
def apply(data, filter, params, limit)
|
|
1420
|
+
case filter
|
|
1421
|
+
when :FlateDecode then unpredict(inflate(data, predicted_limit(params, limit)), params, limit)
|
|
1422
|
+
when :LZWDecode
|
|
1423
|
+
early = int(params[:EarlyChange]) == 0 ? 0 : 1
|
|
1424
|
+
unpredict(QrReaderKernels.lzw_decode(data, predicted_limit(params, limit), early, false), params, limit)
|
|
1425
|
+
when :RunLengthDecode then run_length(data, limit)
|
|
1426
|
+
when :ASCIIHexDecode then ascii_hex(data)
|
|
1427
|
+
when :ASCII85Decode then ascii85(data)
|
|
1428
|
+
when :Crypt then data
|
|
1429
|
+
else raise UnsupportedImageError, "unsupported PDF filter /#{filter}"
|
|
1430
|
+
end
|
|
1431
|
+
end
|
|
1432
|
+
|
|
1433
|
+
# Room for the PNG predictor's tag bytes.
|
|
1434
|
+
def predicted_limit(params, limit)
|
|
1435
|
+
(int(params[:Predictor]) || 1) >= 10 ? limit * 2 + 64 : limit
|
|
1436
|
+
end
|
|
1437
|
+
|
|
1438
|
+
def inflate(data, limit)
|
|
1439
|
+
inflate_with(Zlib::Inflate.new(Zlib::MAX_WBITS + 32), data, limit) ||
|
|
1440
|
+
inflate_with(Zlib::Inflate.new(-Zlib::MAX_WBITS), data, limit) ||
|
|
1441
|
+
raise(ImageError, "corrupt Flate data")
|
|
1442
|
+
end
|
|
1443
|
+
|
|
1444
|
+
# Inflated data (partial for truncated or damaged streams), nil if
|
|
1445
|
+
# nothing could be inflated.
|
|
1446
|
+
def inflate_with(zstream, data, limit)
|
|
1447
|
+
out = String.new(encoding: Encoding::BINARY)
|
|
1448
|
+
full = false
|
|
1449
|
+
begin
|
|
1450
|
+
zstream.inflate(data) do |chunk|
|
|
1451
|
+
out << chunk
|
|
1452
|
+
full = out.bytesize >= limit
|
|
1453
|
+
break if full
|
|
1454
|
+
end
|
|
1455
|
+
rescue Zlib::Error
|
|
1456
|
+
failed = true
|
|
1457
|
+
end
|
|
1458
|
+
# Output of an unfinished stream is not yielded.
|
|
1459
|
+
out << zstream.flush_next_out.to_s unless full
|
|
1460
|
+
failed && out.empty? ? nil : out
|
|
1461
|
+
ensure
|
|
1462
|
+
zstream.reset unless zstream.closed? || zstream.finished?
|
|
1463
|
+
zstream.close unless zstream.closed?
|
|
1464
|
+
end
|
|
1465
|
+
|
|
1466
|
+
def unpredict(data, params, limit)
|
|
1467
|
+
predictor = int(params[:Predictor]) || 1
|
|
1468
|
+
return data if predictor < 2
|
|
1469
|
+
|
|
1470
|
+
colors = (int(params[:Colors]) || 1).clamp(1, 64)
|
|
1471
|
+
bpc = int(params[:BitsPerComponent]) || 8
|
|
1472
|
+
raise ImageError, "invalid predictor BitsPerComponent #{bpc}" unless [1, 2, 4, 8, 16].include?(bpc)
|
|
1473
|
+
|
|
1474
|
+
columns = int(params[:Columns]) || 1
|
|
1475
|
+
columns = 1 if columns < 1
|
|
1476
|
+
row = (columns * colors * bpc + 7) / 8
|
|
1477
|
+
# (Rows are allocated whole: one longer than the output limit
|
|
1478
|
+
# would be a crafted allocation, never an image.)
|
|
1479
|
+
raise ImageError, "invalid predictor Columns #{columns}" if row > limit + 1
|
|
1480
|
+
if predictor == 2
|
|
1481
|
+
tiff_unpredict(data, row, colors, bpc)
|
|
1482
|
+
else
|
|
1483
|
+
png_unpredict(data, row, [(colors * bpc + 7) / 8, 1].max, limit)
|
|
1484
|
+
end
|
|
1485
|
+
end
|
|
1486
|
+
|
|
1487
|
+
def png_unpredict(data, row, bpp, limit)
|
|
1488
|
+
src_row = row + 1
|
|
1489
|
+
rows = [(data.bytesize + row - 1) / src_row, limit / row + 1].min
|
|
1490
|
+
return "".b if rows <= 0
|
|
1491
|
+
|
|
1492
|
+
missing = rows * src_row - data.bytesize
|
|
1493
|
+
data += "\0".b * missing if missing.positive?
|
|
1494
|
+
out = "\0".b * (rows * row)
|
|
1495
|
+
r = 0
|
|
1496
|
+
while r < rows
|
|
1497
|
+
src = r * src_row
|
|
1498
|
+
png_row(data.getbyte(src), data, src + 1, out, r * row, row, bpp, r.positive?)
|
|
1499
|
+
r += 1
|
|
1500
|
+
end
|
|
1501
|
+
out
|
|
1502
|
+
end
|
|
1503
|
+
|
|
1504
|
+
# Reverses one PNG-filtered row (types 0-4) into +out+ at +dst+.
|
|
1505
|
+
def png_row(type, src, so, out, dst, len, bpp, has_up)
|
|
1506
|
+
case type
|
|
1507
|
+
when 1 then bpp == 1 ? png_sub1(src, so, out, dst, len) : png_sub(src, so, out, dst, len, bpp)
|
|
1508
|
+
when 2
|
|
1509
|
+
return out.bytesplice(dst, len, src.byteslice(so, len)) unless has_up
|
|
1510
|
+
|
|
1511
|
+
nz = src.index(NONZERO, so)
|
|
1512
|
+
return out.bytesplice(dst, len, out.byteslice(dst - len, len)) if nz.nil? || nz >= so + len
|
|
1513
|
+
|
|
1514
|
+
png_up(src, so, out, dst, len)
|
|
1515
|
+
when 3
|
|
1516
|
+
i = 0
|
|
1517
|
+
while i < len
|
|
1518
|
+
left = i >= bpp ? out.getbyte(dst + i - bpp) : 0
|
|
1519
|
+
up = has_up ? out.getbyte(dst - len + i) : 0
|
|
1520
|
+
out.setbyte(dst + i, (src.getbyte(so + i) + ((left + up) >> 1)) & 255)
|
|
1521
|
+
i += 1
|
|
1522
|
+
end
|
|
1523
|
+
when 4
|
|
1524
|
+
i = 0
|
|
1525
|
+
while i < len
|
|
1526
|
+
a = i >= bpp ? out.getbyte(dst + i - bpp) : 0
|
|
1527
|
+
b = has_up ? out.getbyte(dst - len + i) : 0
|
|
1528
|
+
c = has_up && i >= bpp ? out.getbyte(dst - len + i - bpp) : 0
|
|
1529
|
+
p = a + b - c
|
|
1530
|
+
pa = (p - a).abs
|
|
1531
|
+
pb = (p - b).abs
|
|
1532
|
+
pc = (p - c).abs
|
|
1533
|
+
pred = if pa <= pb && pa <= pc then a
|
|
1534
|
+
elsif pb <= pc then b
|
|
1535
|
+
else c
|
|
1536
|
+
end
|
|
1537
|
+
out.setbyte(dst + i, (src.getbyte(so + i) + pred) & 255)
|
|
1538
|
+
i += 1
|
|
1539
|
+
end
|
|
1540
|
+
else
|
|
1541
|
+
out.bytesplice(dst, len, src.byteslice(so, len))
|
|
1542
|
+
end
|
|
1543
|
+
end
|
|
1544
|
+
|
|
1545
|
+
# Sub with one byte per pixel: a running sum.
|
|
1546
|
+
def png_sub1(src, so, out, dst, len)
|
|
1547
|
+
left = 0
|
|
1548
|
+
i = so
|
|
1549
|
+
o = dst
|
|
1550
|
+
stop = so + len
|
|
1551
|
+
while i < stop
|
|
1552
|
+
left = (src.getbyte(i) + left) & 255
|
|
1553
|
+
out.setbyte(o, left)
|
|
1554
|
+
i += 1
|
|
1555
|
+
o += 1
|
|
1556
|
+
end
|
|
1557
|
+
end
|
|
1558
|
+
|
|
1559
|
+
def png_sub(src, so, out, dst, len, bpp)
|
|
1560
|
+
out.bytesplice(dst, bpp, src.byteslice(so, bpp))
|
|
1561
|
+
i = bpp
|
|
1562
|
+
while i < len
|
|
1563
|
+
out.setbyte(dst + i, (src.getbyte(so + i) + out.getbyte(dst + i - bpp)) & 255)
|
|
1564
|
+
i += 1
|
|
1565
|
+
end
|
|
1566
|
+
end
|
|
1567
|
+
|
|
1568
|
+
def png_up(src, so, out, dst, len)
|
|
1569
|
+
i = so
|
|
1570
|
+
o = dst
|
|
1571
|
+
u = dst - len
|
|
1572
|
+
stop = so + len
|
|
1573
|
+
while i < stop
|
|
1574
|
+
out.setbyte(o, (src.getbyte(i) + out.getbyte(u)) & 255)
|
|
1575
|
+
i += 1
|
|
1576
|
+
o += 1
|
|
1577
|
+
u += 1
|
|
1578
|
+
end
|
|
1579
|
+
end
|
|
1580
|
+
|
|
1581
|
+
# TIFF predictor 2: horizontal differencing per component.
|
|
1582
|
+
def tiff_unpredict(data, row, colors, bpc)
|
|
1583
|
+
out = data.dup
|
|
1584
|
+
rows = out.bytesize / row
|
|
1585
|
+
r = 0
|
|
1586
|
+
while r < rows
|
|
1587
|
+
case bpc
|
|
1588
|
+
when 8 then tiff_row8(out, r * row, row, colors)
|
|
1589
|
+
when 16 then tiff_row16(out, r * row, row, colors)
|
|
1590
|
+
else tiff_row_bits(out, r * row, row, colors, bpc)
|
|
1591
|
+
end
|
|
1592
|
+
r += 1
|
|
1593
|
+
end
|
|
1594
|
+
out
|
|
1595
|
+
end
|
|
1596
|
+
|
|
1597
|
+
def tiff_row8(out, off, len, colors)
|
|
1598
|
+
i = colors
|
|
1599
|
+
while i < len
|
|
1600
|
+
out.setbyte(off + i, (out.getbyte(off + i) + out.getbyte(off + i - colors)) & 255)
|
|
1601
|
+
i += 1
|
|
1602
|
+
end
|
|
1603
|
+
end
|
|
1604
|
+
|
|
1605
|
+
def tiff_row16(out, off, len, colors)
|
|
1606
|
+
step = 2 * colors
|
|
1607
|
+
i = step
|
|
1608
|
+
while i + 1 < len
|
|
1609
|
+
v = ((out.getbyte(off + i) << 8) | out.getbyte(off + i + 1)) +
|
|
1610
|
+
((out.getbyte(off + i - step) << 8) | out.getbyte(off + i - step + 1))
|
|
1611
|
+
out.setbyte(off + i, (v >> 8) & 255)
|
|
1612
|
+
out.setbyte(off + i + 1, v & 255)
|
|
1613
|
+
i += 2
|
|
1614
|
+
end
|
|
1615
|
+
end
|
|
1616
|
+
|
|
1617
|
+
def tiff_row_bits(out, off, len, colors, bpc)
|
|
1618
|
+
mask = (1 << bpc) - 1
|
|
1619
|
+
samples = len * 8 / bpc
|
|
1620
|
+
i = colors
|
|
1621
|
+
while i < samples
|
|
1622
|
+
v = (sample(out, off, i, bpc) + sample(out, off, i - colors, bpc)) & mask
|
|
1623
|
+
bit = i * bpc
|
|
1624
|
+
shift = 8 - bpc - (bit & 7)
|
|
1625
|
+
byte = off + (bit >> 3)
|
|
1626
|
+
out.setbyte(byte, (out.getbyte(byte) & ~(mask << shift)) | (v << shift))
|
|
1627
|
+
i += 1
|
|
1628
|
+
end
|
|
1629
|
+
end
|
|
1630
|
+
|
|
1631
|
+
def sample(data, off, i, bpc)
|
|
1632
|
+
bit = i * bpc
|
|
1633
|
+
(data.getbyte(off + (bit >> 3)) >> (8 - bpc - (bit & 7))) & ((1 << bpc) - 1)
|
|
1634
|
+
end
|
|
1635
|
+
|
|
1636
|
+
def run_length(data, limit)
|
|
1637
|
+
out = String.new(encoding: Encoding::BINARY)
|
|
1638
|
+
pos = 0
|
|
1639
|
+
n = data.bytesize
|
|
1640
|
+
while pos < n && out.bytesize < limit
|
|
1641
|
+
len = data.getbyte(pos)
|
|
1642
|
+
pos += 1
|
|
1643
|
+
if len < 128
|
|
1644
|
+
out << data.byteslice(pos, len + 1)
|
|
1645
|
+
pos += len + 1
|
|
1646
|
+
elsif len > 128
|
|
1647
|
+
break if pos >= n
|
|
1648
|
+
|
|
1649
|
+
out << (data.byteslice(pos, 1) * (257 - len))
|
|
1650
|
+
pos += 1
|
|
1651
|
+
else
|
|
1652
|
+
break
|
|
1653
|
+
end
|
|
1654
|
+
end
|
|
1655
|
+
out
|
|
1656
|
+
end
|
|
1657
|
+
|
|
1658
|
+
def ascii_hex(data)
|
|
1659
|
+
stop = data.index(">")
|
|
1660
|
+
data = data.byteslice(0, stop) if stop
|
|
1661
|
+
hex = data.delete(HEX_DIGITS)
|
|
1662
|
+
hex << "0" if hex.bytesize.odd?
|
|
1663
|
+
[hex].pack("H*")
|
|
1664
|
+
end
|
|
1665
|
+
|
|
1666
|
+
def ascii85(data)
|
|
1667
|
+
stop = data.index("~>")
|
|
1668
|
+
data = data.byteslice(0, stop) if stop
|
|
1669
|
+
data = data.delete(WHITESPACE)
|
|
1670
|
+
data = data.byteslice(2, data.bytesize - 2) if data.start_with?("<~")
|
|
1671
|
+
data = data.gsub("z", "!!!!!") if data.include?("z")
|
|
1672
|
+
bad = data.index(/[^!-u]/)
|
|
1673
|
+
data = data.byteslice(0, bad) if bad
|
|
1674
|
+
full = data.bytesize / 5
|
|
1675
|
+
words = []
|
|
1676
|
+
pos = 0
|
|
1677
|
+
while pos < full
|
|
1678
|
+
stop = [pos + 1024, full].min
|
|
1679
|
+
ascii85_groups(data, pos, stop, words)
|
|
1680
|
+
pos = stop
|
|
1681
|
+
end
|
|
1682
|
+
out = words.pack("N*")
|
|
1683
|
+
rest = data.bytesize - full * 5
|
|
1684
|
+
if rest > 1
|
|
1685
|
+
tail = []
|
|
1686
|
+
ascii85_groups(data.byteslice(full * 5, rest) + ("u" * (5 - rest)), 0, 1, tail)
|
|
1687
|
+
out << tail.pack("N").byteslice(0, rest - 1)
|
|
1688
|
+
end
|
|
1689
|
+
out
|
|
1690
|
+
end
|
|
1691
|
+
|
|
1692
|
+
def ascii85_groups(data, group, stop, words)
|
|
1693
|
+
while group < stop
|
|
1694
|
+
o = group * 5
|
|
1695
|
+
v = data.getbyte(o) - 33
|
|
1696
|
+
v = v * 85 + data.getbyte(o + 1) - 33
|
|
1697
|
+
v = v * 85 + data.getbyte(o + 2) - 33
|
|
1698
|
+
v = v * 85 + data.getbyte(o + 3) - 33
|
|
1699
|
+
v = v * 85 + data.getbyte(o + 4) - 33
|
|
1700
|
+
words << (v & 0xFFFFFFFF)
|
|
1701
|
+
group += 1
|
|
1702
|
+
end
|
|
1703
|
+
end
|
|
1704
|
+
end
|
|
1705
|
+
end
|
|
1706
|
+
|
|
1707
|
+
# Colour spaces reduced to what luminance needs.
|
|
1708
|
+
class ColorSpace
|
|
1709
|
+
attr_reader :kind, :components
|
|
1710
|
+
|
|
1711
|
+
def initialize(kind, components, palette: nil, none: false)
|
|
1712
|
+
@kind = kind
|
|
1713
|
+
@components = components
|
|
1714
|
+
@palette = palette
|
|
1715
|
+
@none = none
|
|
1716
|
+
end
|
|
1717
|
+
|
|
1718
|
+
GRAY = new(:gray, 1)
|
|
1719
|
+
RGB = new(:rgb, 3)
|
|
1720
|
+
CMYK = new(:cmyk, 4)
|
|
1721
|
+
LAB = new(:lab, 3)
|
|
1722
|
+
MASK = new(:gray, 1)
|
|
1723
|
+
|
|
1724
|
+
class << self
|
|
1725
|
+
# Colour space of an image's /ColorSpace value (names are looked
|
|
1726
|
+
# up in the resources); nil when the value is missing.
|
|
1727
|
+
def build(doc, spec, resources, depth = 0)
|
|
1728
|
+
spec = doc.resolve(spec)
|
|
1729
|
+
return nil if spec.nil?
|
|
1730
|
+
raise UnsupportedImageError, "colour spaces nested too deeply" if depth > 8
|
|
1731
|
+
|
|
1732
|
+
family = spec.is_a?(Array) ? doc.resolve(spec[0]) : spec
|
|
1733
|
+
case family
|
|
1734
|
+
when :DeviceGray, :CalGray, :G then GRAY
|
|
1735
|
+
when :DeviceRGB, :CalRGB, :RGB then RGB
|
|
1736
|
+
when :DeviceCMYK, :CMYK then CMYK
|
|
1737
|
+
when :Lab then LAB
|
|
1738
|
+
when :ICCBased then icc(doc, spec, resources, depth)
|
|
1739
|
+
when :Indexed, :I then indexed(doc, spec, resources, depth)
|
|
1740
|
+
when :Separation then new(:separation, 1, none: spec.is_a?(Array) && doc.resolve(spec[1]) == :None)
|
|
1741
|
+
when :DeviceN then device_n(doc, spec)
|
|
1742
|
+
when :Pattern then raise UnsupportedImageError, "image with a pattern colour space"
|
|
1743
|
+
when Symbol
|
|
1744
|
+
named = doc.dict_value(doc.dict_value(resources)&.[](:ColorSpace))&.[](family)
|
|
1745
|
+
raise UnsupportedImageError, "unknown colour space /#{family}" if named.nil?
|
|
1746
|
+
|
|
1747
|
+
build(doc, named, resources, depth + 1)
|
|
1748
|
+
else raise UnsupportedImageError, "invalid colour space"
|
|
1749
|
+
end
|
|
1750
|
+
end
|
|
1751
|
+
|
|
1752
|
+
# Luminance of CMYK ink amounts (0..255), with the formula of the
|
|
1753
|
+
# JPEG decoder (which sees Adobe's inverted CMYK).
|
|
1754
|
+
def cmyk_luma(c, m, y, k)
|
|
1755
|
+
kk = 255 - k
|
|
1756
|
+
Image.luma(((255 - c) * kk + 127) / 255, ((255 - m) * kk + 127) / 255, ((255 - y) * kk + 127) / 255)
|
|
1757
|
+
end
|
|
1758
|
+
|
|
1759
|
+
private
|
|
1760
|
+
|
|
1761
|
+
def icc(doc, spec, resources, depth)
|
|
1762
|
+
stream = spec.is_a?(Array) ? doc.resolve(spec[1]) : nil
|
|
1763
|
+
dict = stream.is_a?(Stream) ? stream.dict : {}
|
|
1764
|
+
case doc.resolve(dict[:N])
|
|
1765
|
+
when 1 then GRAY
|
|
1766
|
+
when 3 then RGB
|
|
1767
|
+
when 4 then CMYK
|
|
1768
|
+
else
|
|
1769
|
+
alternate = dict[:Alternate] or raise UnsupportedImageError, "ICCBased colour space without /N"
|
|
1770
|
+
build(doc, alternate, resources, depth + 1)
|
|
1771
|
+
end
|
|
1772
|
+
end
|
|
1773
|
+
|
|
1774
|
+
def indexed(doc, spec, resources, depth)
|
|
1775
|
+
raise UnsupportedImageError, "invalid Indexed colour space" unless spec.is_a?(Array) && spec.size >= 4
|
|
1776
|
+
|
|
1777
|
+
base = build(doc, spec[1], resources, depth + 1)
|
|
1778
|
+
raise UnsupportedImageError, "invalid Indexed base colour space" if base.nil? || base.kind == :indexed
|
|
1779
|
+
|
|
1780
|
+
hival = Util.int(doc.resolve(spec[2]))&.clamp(0, 255) || 0
|
|
1781
|
+
lookup = doc.resolve(spec[3])
|
|
1782
|
+
lookup = doc.decode_stream(lookup, (hival + 1) * base.components) if lookup.is_a?(Stream)
|
|
1783
|
+
raise UnsupportedImageError, "invalid Indexed lookup table" unless lookup.is_a?(String)
|
|
1784
|
+
|
|
1785
|
+
n = base.components
|
|
1786
|
+
palette = Array.new(hival + 1) { |i| base.luma_at(lookup, i * n) }
|
|
1787
|
+
new(:indexed, 1, palette: palette)
|
|
1788
|
+
end
|
|
1789
|
+
|
|
1790
|
+
def device_n(doc, spec)
|
|
1791
|
+
names = spec.is_a?(Array) ? doc.resolve(spec[1]) : nil
|
|
1792
|
+
unless names.is_a?(Array) && names.size.between?(1, 32)
|
|
1793
|
+
raise UnsupportedImageError, "invalid DeviceN colour space"
|
|
1794
|
+
end
|
|
1795
|
+
|
|
1796
|
+
new(:device_n, names.size, none: names.all? { |n| doc.resolve(n) == :None })
|
|
1797
|
+
end
|
|
1798
|
+
end
|
|
1799
|
+
|
|
1800
|
+
# [min, max] decode range per component.
|
|
1801
|
+
def decode_ranges(decode, bpc)
|
|
1802
|
+
if decode.is_a?(Array) && decode.size >= 2 * @components
|
|
1803
|
+
ranges = decode.first(2 * @components).map { |v| Util.number(v) }
|
|
1804
|
+
return ranges.each_slice(2).to_a unless ranges.include?(nil)
|
|
1805
|
+
end
|
|
1806
|
+
case @kind
|
|
1807
|
+
when :indexed then [[0.0, ((1 << bpc) - 1).to_f]]
|
|
1808
|
+
when :lab then [[0.0, 100.0], [-100.0, 100.0], [-100.0, 100.0]]
|
|
1809
|
+
else [[0.0, 1.0]] * @components
|
|
1810
|
+
end
|
|
1811
|
+
end
|
|
1812
|
+
|
|
1813
|
+
# Per component: sample value (the high byte for 16 bits) ->
|
|
1814
|
+
# component value 0..255 (or palette index).
|
|
1815
|
+
def component_luts(bpc, decode)
|
|
1816
|
+
maxv = ((1 << bpc) - 1).to_f
|
|
1817
|
+
size = bpc == 16 ? 256 : (1 << bpc)
|
|
1818
|
+
step = bpc == 16 ? 257 : 1
|
|
1819
|
+
decode_ranges(decode, bpc).each_with_index.map do |(dmin, dmax), c|
|
|
1820
|
+
Array.new(size) { |s| normalize(c, dmin + (s * step) * (dmax - dmin) / maxv) }
|
|
1821
|
+
end
|
|
1822
|
+
end
|
|
1823
|
+
|
|
1824
|
+
# Single-component spaces: sample value -> luminance.
|
|
1825
|
+
def luma_lut(bpc, decode)
|
|
1826
|
+
component_luts(bpc, decode).first.map { |v| luma1(v) }
|
|
1827
|
+
end
|
|
1828
|
+
|
|
1829
|
+
# True if the decode array inverts every component.
|
|
1830
|
+
def inverted?(decode, bpc = 8)
|
|
1831
|
+
decode_ranges(decode, bpc).all? { |dmin, dmax| dmin > dmax }
|
|
1832
|
+
end
|
|
1833
|
+
|
|
1834
|
+
# Luminance of the 8-bit components at +offset+ of a lookup table.
|
|
1835
|
+
def luma_at(bytes, offset)
|
|
1836
|
+
c = Array.new(@components) { |i| bytes.getbyte(offset + i) || 0 }
|
|
1837
|
+
case @kind
|
|
1838
|
+
when :gray, :lab then c[0]
|
|
1839
|
+
when :rgb then Image.luma(c[0], c[1], c[2])
|
|
1840
|
+
when :cmyk then ColorSpace.cmyk_luma(c[0], c[1], c[2], c[3])
|
|
1841
|
+
else @none ? 255 : 255 - c.max
|
|
1842
|
+
end
|
|
1843
|
+
end
|
|
1844
|
+
|
|
1845
|
+
private
|
|
1846
|
+
|
|
1847
|
+
def normalize(component, value)
|
|
1848
|
+
case @kind
|
|
1849
|
+
when :indexed then value.round.clamp(0, @palette.size - 1)
|
|
1850
|
+
when :lab then component.zero? ? (value * 2.55).round.clamp(0, 255) : 0
|
|
1851
|
+
else (value * 255).round.clamp(0, 255)
|
|
1852
|
+
end
|
|
1853
|
+
end
|
|
1854
|
+
|
|
1855
|
+
def luma1(value)
|
|
1856
|
+
case @kind
|
|
1857
|
+
when :indexed then @palette[value]
|
|
1858
|
+
when :separation, :device_n then @none ? 255 : 255 - value
|
|
1859
|
+
else value
|
|
1860
|
+
end
|
|
1861
|
+
end
|
|
1862
|
+
end
|
|
1863
|
+
|
|
1864
|
+
# Per-row pixel loops (YJIT compiles methods that are called often,
|
|
1865
|
+
# never a loop in a method entered once).
|
|
1866
|
+
module Pixels
|
|
1867
|
+
# Byte -> its four 2-bit samples, as Integers.
|
|
1868
|
+
TWO_BIT = Array.new(256) { |b| [b >> 6, (b >> 4) & 3, (b >> 2) & 3, b & 3] }.freeze
|
|
1869
|
+
|
|
1870
|
+
module_function
|
|
1871
|
+
|
|
1872
|
+
def rgb(src, off, n, out)
|
|
1873
|
+
i = off
|
|
1874
|
+
stop = off + 3 * n
|
|
1875
|
+
while i < stop
|
|
1876
|
+
out << ((306 * src.getbyte(i) + 601 * src.getbyte(i + 1) + 117 * src.getbyte(i + 2) + 0x200) >> 10)
|
|
1877
|
+
i += 3
|
|
1878
|
+
end
|
|
1879
|
+
end
|
|
1880
|
+
|
|
1881
|
+
def cmyk(src, off, n, out)
|
|
1882
|
+
i = off
|
|
1883
|
+
stop = off + 4 * n
|
|
1884
|
+
while i < stop
|
|
1885
|
+
kk = 255 - src.getbyte(i + 3)
|
|
1886
|
+
r = ((255 - src.getbyte(i)) * kk + 127) / 255
|
|
1887
|
+
g = ((255 - src.getbyte(i + 1)) * kk + 127) / 255
|
|
1888
|
+
b = ((255 - src.getbyte(i + 2)) * kk + 127) / 255
|
|
1889
|
+
out << ((306 * r + 601 * g + 117 * b + 0x200) >> 10)
|
|
1890
|
+
i += 4
|
|
1891
|
+
end
|
|
1892
|
+
end
|
|
1893
|
+
|
|
1894
|
+
# Every +stride+-th byte (Lab: L*).
|
|
1895
|
+
def first(src, off, n, out, stride)
|
|
1896
|
+
i = off
|
|
1897
|
+
stop = off + stride * n
|
|
1898
|
+
while i < stop
|
|
1899
|
+
out << src.getbyte(i)
|
|
1900
|
+
i += stride
|
|
1901
|
+
end
|
|
1902
|
+
end
|
|
1903
|
+
|
|
1904
|
+
# DeviceN: the darkest tint wins.
|
|
1905
|
+
def device_n(src, off, n, out, stride)
|
|
1906
|
+
i = off
|
|
1907
|
+
stop = off + stride * n
|
|
1908
|
+
while i < stop
|
|
1909
|
+
max = 0
|
|
1910
|
+
j = i
|
|
1911
|
+
e = i + stride
|
|
1912
|
+
while j < e
|
|
1913
|
+
v = src.getbyte(j)
|
|
1914
|
+
max = v if v > max
|
|
1915
|
+
j += 1
|
|
1916
|
+
end
|
|
1917
|
+
out << (255 - max)
|
|
1918
|
+
i += stride
|
|
1919
|
+
end
|
|
1920
|
+
end
|
|
1921
|
+
|
|
1922
|
+
def high_bytes(src, off, n, out)
|
|
1923
|
+
i = off
|
|
1924
|
+
stop = off + 2 * n
|
|
1925
|
+
while i < stop
|
|
1926
|
+
out << src.getbyte(i)
|
|
1927
|
+
i += 2
|
|
1928
|
+
end
|
|
1929
|
+
end
|
|
1930
|
+
|
|
1931
|
+
def two_bit(src, off, nbytes, out, lut)
|
|
1932
|
+
i = off
|
|
1933
|
+
stop = off + nbytes
|
|
1934
|
+
while i < stop
|
|
1935
|
+
b = src.getbyte(i)
|
|
1936
|
+
out << lut[b >> 6] << lut[(b >> 4) & 3] << lut[(b >> 2) & 3] << lut[b & 3]
|
|
1937
|
+
i += 1
|
|
1938
|
+
end
|
|
1939
|
+
end
|
|
1940
|
+
|
|
1941
|
+
# Colour conversion with a separate lookup table per component.
|
|
1942
|
+
def mapped(src, off, n, out, kind, luts)
|
|
1943
|
+
ncomp = luts.size
|
|
1944
|
+
i = off
|
|
1945
|
+
stop = off + ncomp * n
|
|
1946
|
+
c = Array.new(ncomp, 0)
|
|
1947
|
+
while i < stop
|
|
1948
|
+
k = 0
|
|
1949
|
+
while k < ncomp
|
|
1950
|
+
c[k] = luts[k][src.getbyte(i + k)]
|
|
1951
|
+
k += 1
|
|
1952
|
+
end
|
|
1953
|
+
out << case kind
|
|
1954
|
+
when :rgb then Image.luma(c[0], c[1], c[2])
|
|
1955
|
+
when :cmyk then ColorSpace.cmyk_luma(c[0], c[1], c[2], c[3])
|
|
1956
|
+
when :lab then c[0]
|
|
1957
|
+
else 255 - c.max
|
|
1958
|
+
end
|
|
1959
|
+
i += ncomp
|
|
1960
|
+
end
|
|
1961
|
+
end
|
|
1962
|
+
end
|
|
1963
|
+
|
|
1964
|
+
# Runs content streams and collects the images they draw.
|
|
1965
|
+
class ContentScanner
|
|
1966
|
+
MAX_FORM_DEPTH = 12
|
|
1967
|
+
MAX_OPERANDS = 32
|
|
1968
|
+
MAX_STACK = 256
|
|
1969
|
+
IDENTITY = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0].freeze
|
|
1970
|
+
INLINE_KEYS = {
|
|
1971
|
+
BPC: :BitsPerComponent, CS: :ColorSpace, D: :Decode, DP: :DecodeParms, F: :Filter,
|
|
1972
|
+
H: :Height, IM: :ImageMask, I: :Interpolate, W: :Width, L: :Length
|
|
1973
|
+
}.freeze
|
|
1974
|
+
EI = %r{EI(?![^\x00\t\n\f\r ()<>\[\]{}/%])}
|
|
1975
|
+
EI_AFTER_SPACE = %r{[\x00\t\n\f\r ]EI(?![^\x00\t\n\f\r ()<>\[\]{}/%])}
|
|
1976
|
+
TEXTUAL = /\A[\t\n\r\x20-\x7E]*\z/
|
|
1977
|
+
STRING_SPECIAL = /[()\\]/
|
|
1978
|
+
|
|
1979
|
+
def initialize(doc)
|
|
1980
|
+
@doc = doc
|
|
1981
|
+
@images = []
|
|
1982
|
+
@seen = {}
|
|
1983
|
+
@forms = {}
|
|
1984
|
+
end
|
|
1985
|
+
|
|
1986
|
+
def images(content, resources)
|
|
1987
|
+
scan(content, resources, IDENTITY, 0)
|
|
1988
|
+
@images
|
|
1989
|
+
end
|
|
1990
|
+
|
|
1991
|
+
private
|
|
1992
|
+
|
|
1993
|
+
def scan(data, resources, ctm, depth)
|
|
1994
|
+
s = StringScanner.new(data)
|
|
1995
|
+
operands = []
|
|
1996
|
+
stack = []
|
|
1997
|
+
space_run = Parser::SPACE_RUN
|
|
1998
|
+
space_byte = Parser::SPACE_BYTE
|
|
1999
|
+
token_run = Parser::TOKEN_RUN
|
|
2000
|
+
delimiter_byte = Parser::DELIMITER_BYTE
|
|
2001
|
+
while true
|
|
2002
|
+
# (Parser.skip_space, with its common case inlined.)
|
|
2003
|
+
s.skip(space_run)
|
|
2004
|
+
b = data.getbyte(s.pos) or break
|
|
2005
|
+
b = Parser.skip_space(s, data) or break if b == 0x25 || space_byte[b]
|
|
2006
|
+
case b
|
|
2007
|
+
when 0x2F then operand(operands, Parser.scan_token(s, data, Parser::NAME_RUN))
|
|
2008
|
+
when 0x28
|
|
2009
|
+
skip_string(s, data)
|
|
2010
|
+
operand(operands, nil)
|
|
2011
|
+
when 0x3C
|
|
2012
|
+
if data.getbyte(s.pos + 1) == 0x3C
|
|
2013
|
+
s.pos += 2
|
|
2014
|
+
else
|
|
2015
|
+
s.skip_until(/>/) || s.terminate
|
|
2016
|
+
operand(operands, nil)
|
|
2017
|
+
end
|
|
2018
|
+
when 0x3E, 0x5B, 0x5D, 0x7B, 0x7D, 0x29, 0x25 then s.pos += 1
|
|
2019
|
+
else
|
|
2020
|
+
# (Parser.scan_token inlined.)
|
|
2021
|
+
token = s.scan(token_run)
|
|
2022
|
+
n = data.getbyte(s.pos)
|
|
2023
|
+
s.pos = data.index(Parser::DELIMITER, s.pos) || data.bytesize if n && !delimiter_byte[n]
|
|
2024
|
+
if b <= 0x39 && (b >= 0x30 || b == 0x2B || b == 0x2D || b == 0x2E)
|
|
2025
|
+
operands << token
|
|
2026
|
+
operands.shift if operands.size > MAX_OPERANDS
|
|
2027
|
+
next
|
|
2028
|
+
end
|
|
2029
|
+
case token
|
|
2030
|
+
when "cm" then ctm = multiply(operands.last(6).map(&:to_f), ctm) if operands.size >= 6
|
|
2031
|
+
when "q" then stack << ctm if stack.size < MAX_STACK
|
|
2032
|
+
when "Q" then ctm = stack.pop || ctm
|
|
2033
|
+
when "Do" then draw(operands.last, resources, ctm, depth)
|
|
2034
|
+
when "BI" then inline_image(s, data, resources, ctm)
|
|
2035
|
+
end
|
|
2036
|
+
operands.clear
|
|
2037
|
+
end
|
|
2038
|
+
end
|
|
2039
|
+
end
|
|
2040
|
+
|
|
2041
|
+
# Keeps the last MAX_OPERANDS operands (operators take at most 6).
|
|
2042
|
+
def operand(operands, value)
|
|
2043
|
+
operands << value
|
|
2044
|
+
operands.shift if operands.size > MAX_OPERANDS
|
|
2045
|
+
end
|
|
2046
|
+
|
|
2047
|
+
def skip_string(s, data)
|
|
2048
|
+
s.pos += 1
|
|
2049
|
+
depth = 1
|
|
2050
|
+
while depth.positive?
|
|
2051
|
+
unless s.skip_until(STRING_SPECIAL)
|
|
2052
|
+
s.terminate
|
|
2053
|
+
return
|
|
2054
|
+
end
|
|
2055
|
+
case data.getbyte(s.pos - 1)
|
|
2056
|
+
when 0x5C then s.pos += 1 unless s.eos?
|
|
2057
|
+
when 0x28 then depth += 1
|
|
2058
|
+
else depth -= 1
|
|
2059
|
+
end
|
|
2060
|
+
end
|
|
2061
|
+
end
|
|
2062
|
+
|
|
2063
|
+
# m x ctm (both [a b c d e f]).
|
|
2064
|
+
def multiply(m, n)
|
|
2065
|
+
a, b, c, d, e, f = m
|
|
2066
|
+
na, nb, nc, nd, ne, nf = n
|
|
2067
|
+
[a * na + b * nc, a * nb + b * nd, c * na + d * nc, c * nb + d * nd,
|
|
2068
|
+
e * na + f * nc + ne, e * nb + f * nd + nf]
|
|
2069
|
+
end
|
|
2070
|
+
|
|
2071
|
+
def draw(token, resources, ctm, depth)
|
|
2072
|
+
return unless token.is_a?(String)
|
|
2073
|
+
|
|
2074
|
+
name = Parser.name_symbol(token)
|
|
2075
|
+
xobjects = @doc.dict_value(@doc.dict_value(resources)&.[](:XObject)) or return
|
|
2076
|
+
ref = xobjects[name]
|
|
2077
|
+
stream = @doc.resolve(ref)
|
|
2078
|
+
return unless stream.is_a?(Stream)
|
|
2079
|
+
|
|
2080
|
+
key = ref.is_a?(Ref) ? ref : stream.object_id
|
|
2081
|
+
dict = stream.dict
|
|
2082
|
+
subtype = @doc.resolve(dict[:Subtype])
|
|
2083
|
+
if subtype == :Image || (subtype.nil? && dict.key?(:Width))
|
|
2084
|
+
return if @seen[key]
|
|
2085
|
+
|
|
2086
|
+
@seen[key] = true
|
|
2087
|
+
@images << ImageSource.new(dict, stream, nil, resources, name.to_s, ctm)
|
|
2088
|
+
elsif subtype == :Form || subtype.nil?
|
|
2089
|
+
return if depth >= MAX_FORM_DEPTH || @forms[key]
|
|
2090
|
+
|
|
2091
|
+
@forms[key] = true
|
|
2092
|
+
content = @doc.content(stream) or return
|
|
2093
|
+
matrix = @doc.resolve(dict[:Matrix])
|
|
2094
|
+
matrix = matrix.map { |v| Util.number(@doc.resolve(v)) || 0.0 } if matrix.is_a?(Array) && matrix.size == 6
|
|
2095
|
+
ctm = multiply(matrix, ctm) if matrix.is_a?(Array) && matrix.size == 6
|
|
2096
|
+
scan(content, dict.key?(:Resources) ? dict[:Resources] : resources, ctm, depth + 1)
|
|
2097
|
+
end
|
|
2098
|
+
end
|
|
2099
|
+
|
|
2100
|
+
def inline_image(s, data, resources, ctm)
|
|
2101
|
+
parser = Parser.new(data)
|
|
2102
|
+
parser.pos = s.pos
|
|
2103
|
+
dict = {}
|
|
2104
|
+
loop do
|
|
2105
|
+
key = parser.parse
|
|
2106
|
+
break if key.is_a?(Keyword) && key.word == "ID"
|
|
2107
|
+
raise ParseError, "invalid inline image" unless key.is_a?(Symbol) && dict.size < 64
|
|
2108
|
+
|
|
2109
|
+
value = parser.parse
|
|
2110
|
+
raise ParseError, "invalid inline image" if value.is_a?(Keyword)
|
|
2111
|
+
|
|
2112
|
+
dict[INLINE_KEYS.fetch(key, key)] = value
|
|
2113
|
+
end
|
|
2114
|
+
start = parser.pos
|
|
2115
|
+
start += 1 if start < data.bytesize && " \t\n\f\r\x00".include?(data.byteslice(start, 1))
|
|
2116
|
+
stop, after = extent(data, start, dict, resources)
|
|
2117
|
+
s.pos = after
|
|
2118
|
+
@images << ImageSource.new(dict, nil, data.byteslice(start, stop - start), resources, nil, ctm)
|
|
2119
|
+
rescue ParseError
|
|
2120
|
+
s.skip_until(EI) || s.terminate
|
|
2121
|
+
end
|
|
2122
|
+
|
|
2123
|
+
# [end of the image data, position after EI].
|
|
2124
|
+
def extent(data, start, dict, resources)
|
|
2125
|
+
length = Util.int(@doc.resolve(dict[:Length]))
|
|
2126
|
+
if length && length >= 0 && (after = ei_at(data, start + length))
|
|
2127
|
+
return [start + length, after]
|
|
2128
|
+
end
|
|
2129
|
+
|
|
2130
|
+
filters, = @doc.filter_chain(dict)
|
|
2131
|
+
if filters.empty?
|
|
2132
|
+
size = raw_size(dict, resources)
|
|
2133
|
+
return [start + size, after] if size && (after = ei_at(data, start + size))
|
|
2134
|
+
else
|
|
2135
|
+
marker = { ASCIIHexDecode: ">", ASCII85Decode: "~>" }[filters.first]
|
|
2136
|
+
if marker && (stop = data.index(marker, start)) && (after = ei_at(data, stop + marker.bytesize))
|
|
2137
|
+
return [stop + marker.bytesize, after]
|
|
2138
|
+
end
|
|
2139
|
+
end
|
|
2140
|
+
search(data, start)
|
|
2141
|
+
end
|
|
2142
|
+
|
|
2143
|
+
def raw_size(dict, resources)
|
|
2144
|
+
width = Util.positive(@doc.resolve(dict[:Width])) or return nil
|
|
2145
|
+
height = Util.positive(@doc.resolve(dict[:Height])) or return nil
|
|
2146
|
+
mask = @doc.resolve(dict[:ImageMask]) == true
|
|
2147
|
+
bpc = mask ? 1 : Util.positive(@doc.resolve(dict[:BitsPerComponent])) || 8
|
|
2148
|
+
components = mask ? 1 : ColorSpace.build(@doc, dict[:ColorSpace], resources)&.components || 1
|
|
2149
|
+
(width * components * bpc + 7) / 8 * height
|
|
2150
|
+
rescue Error
|
|
2151
|
+
nil
|
|
2152
|
+
end
|
|
2153
|
+
|
|
2154
|
+
# Position after an "EI" at +pos+ (after optional white space).
|
|
2155
|
+
def ei_at(data, pos)
|
|
2156
|
+
return nil if pos > data.bytesize
|
|
2157
|
+
|
|
2158
|
+
pos = data.index(Parser::NOT_SPACE, pos) or return nil
|
|
2159
|
+
return nil unless data.getbyte(pos) == 0x45 && data.getbyte(pos + 1) == 0x49 # EI
|
|
2160
|
+
return nil unless (after = data.getbyte(pos + 2)).nil? || Parser::DELIMITERS.include?(after)
|
|
2161
|
+
|
|
2162
|
+
pos + 2
|
|
2163
|
+
end
|
|
2164
|
+
|
|
2165
|
+
# First " EI" followed by something that looks like content stream
|
|
2166
|
+
# operators (binary image data rarely does).
|
|
2167
|
+
def search(data, start)
|
|
2168
|
+
pos = start
|
|
2169
|
+
while (i = data.index(EI_AFTER_SPACE, pos))
|
|
2170
|
+
after = i + 3
|
|
2171
|
+
return [i, after] if TEXTUAL.match?(data.byteslice(after, 48))
|
|
2172
|
+
|
|
2173
|
+
pos = i + 1
|
|
2174
|
+
end
|
|
2175
|
+
[data.bytesize, data.bytesize]
|
|
2176
|
+
end
|
|
2177
|
+
end
|
|
2178
|
+
|
|
2179
|
+
# Decodes one image (XObject or inline) to a luminance Image.
|
|
2180
|
+
class ImageDecoder
|
|
2181
|
+
include Util
|
|
2182
|
+
|
|
2183
|
+
CODECS = %i[DCTDecode CCITTFaxDecode JBIG2Decode JPXDecode].freeze
|
|
2184
|
+
ALL_BYTES = Image::TR_ESCAPE.call((0..255).to_a)
|
|
2185
|
+
IDENTITY = (0..255).to_a.freeze
|
|
2186
|
+
SOF_MARKERS = [0xC0, 0xC1, 0xC2, 0xC3, 0xC5, 0xC6, 0xC7, 0xC9, 0xCA, 0xCB, 0xCD, 0xCE, 0xCF].freeze
|
|
2187
|
+
|
|
2188
|
+
def initialize(doc, source, raw_jpeg = false)
|
|
2189
|
+
@doc = doc
|
|
2190
|
+
@source = source
|
|
2191
|
+
@dict = source.dict
|
|
2192
|
+
@raw_jpeg = raw_jpeg
|
|
2193
|
+
end
|
|
2194
|
+
|
|
2195
|
+
def decode
|
|
2196
|
+
width = positive(value(:Width))
|
|
2197
|
+
height = positive(value(:Height))
|
|
2198
|
+
raise ImageError, "image without a valid /Width and /Height" unless width && height
|
|
2199
|
+
if width * height > MAX_PIXELS
|
|
2200
|
+
raise UnsupportedImageError, "PDF image too large (#{width}x#{height}, limit #{MAX_PIXELS} pixels)"
|
|
2201
|
+
end
|
|
2202
|
+
|
|
2203
|
+
filters, params = @doc.filter_chain(@dict)
|
|
2204
|
+
codec = CODECS.include?(filters.last) ? filters.pop : nil
|
|
2205
|
+
codec_params = codec ? params.pop : nil
|
|
2206
|
+
mask = value(:ImageMask) == true
|
|
2207
|
+
space = mask ? ColorSpace::MASK : ColorSpace.build(@doc, @dict[:ColorSpace], @source.resources)
|
|
2208
|
+
decode = value(:Decode)
|
|
2209
|
+
decode = decode.map { |v| @doc.resolve(v) } if decode.is_a?(Array)
|
|
2210
|
+
data = @source.stream ? @doc.stream_bytes(@source.stream) : @source.data
|
|
2211
|
+
|
|
2212
|
+
case codec
|
|
2213
|
+
when nil
|
|
2214
|
+
bpc = mask ? 1 : int(value(:BitsPerComponent)) || 8
|
|
2215
|
+
raw_image(data, filters, params, width, height, space || ColorSpace::GRAY, bpc, decode)
|
|
2216
|
+
when :DCTDecode
|
|
2217
|
+
dct_image(Filters.decode(data, filters, params, SAMPLE_LIMIT), width, height, space, decode)
|
|
2218
|
+
when :CCITTFaxDecode
|
|
2219
|
+
data = Filters.decode(data, filters, params, SAMPLE_LIMIT)
|
|
2220
|
+
ccitt_image(data, codec_params, width, height, one_component(space), decode)
|
|
2221
|
+
when :JBIG2Decode
|
|
2222
|
+
data = Filters.decode(data, filters, params, SAMPLE_LIMIT)
|
|
2223
|
+
jbig2_image(data, codec_params, width, height, one_component(space), decode)
|
|
2224
|
+
else
|
|
2225
|
+
raise UnsupportedImageError, "JPEG 2000 (JPXDecode) images are not supported"
|
|
2226
|
+
end
|
|
2227
|
+
end
|
|
2228
|
+
|
|
2229
|
+
private
|
|
2230
|
+
|
|
2231
|
+
def value(key)
|
|
2232
|
+
@doc.resolve(@dict[key])
|
|
2233
|
+
end
|
|
2234
|
+
|
|
2235
|
+
def one_component(space)
|
|
2236
|
+
space && space.components == 1 ? space : ColorSpace::GRAY
|
|
2237
|
+
end
|
|
2238
|
+
|
|
2239
|
+
def raw_image(data, filters, params, width, height, space, bpc, decode)
|
|
2240
|
+
raise UnsupportedImageError, "unsupported BitsPerComponent #{bpc}" unless [1, 2, 4, 8, 16].include?(bpc)
|
|
2241
|
+
|
|
2242
|
+
ncomp = space.components
|
|
2243
|
+
stride = (width * ncomp * bpc + 7) / 8
|
|
2244
|
+
# Packed and unpacked (one byte per sample) buffers are both bounded.
|
|
2245
|
+
if stride * height > SAMPLE_LIMIT || width * ncomp * height > SAMPLE_LIMIT
|
|
2246
|
+
raise UnsupportedImageError, "PDF image too large"
|
|
2247
|
+
end
|
|
2248
|
+
|
|
2249
|
+
raw = Filters.decode(data, filters, params, stride * height)
|
|
2250
|
+
raise ImageError, "image has no data" if raw.empty?
|
|
2251
|
+
|
|
2252
|
+
if ncomp == 1
|
|
2253
|
+
pixels, rows = expand(raw, width, height, bpc, space.luma_lut(bpc, decode))
|
|
2254
|
+
else
|
|
2255
|
+
luts = space.component_luts(bpc, decode)
|
|
2256
|
+
uniform = luts.uniq.size == 1 || space.kind == :lab
|
|
2257
|
+
# Uniform: samples are mapped while unpacking; otherwise they
|
|
2258
|
+
# stay raw values and each component is mapped while converting.
|
|
2259
|
+
lut = uniform ? luts.first : Array.new(luts.first.size) { |s| s }
|
|
2260
|
+
samples, rows = expand(raw, width * ncomp, height, bpc, lut)
|
|
2261
|
+
pixels = convert(samples, width, rows, space, uniform ? nil : luts)
|
|
2262
|
+
end
|
|
2263
|
+
pixels << ("\xFF".b * (width * (height - rows))) if rows < height
|
|
2264
|
+
Image.new(width, height, pixels)
|
|
2265
|
+
end
|
|
2266
|
+
|
|
2267
|
+
# One byte per sample, mapped through +lut+ (indexed by the sample
|
|
2268
|
+
# value, or its high byte for 16 bits), for the rows present in
|
|
2269
|
+
# +raw+ (a partial last row is zero-padded). Returns [samples, rows].
|
|
2270
|
+
def expand(raw, samples_per_row, height, bpc, lut)
|
|
2271
|
+
stride = (samples_per_row * bpc + 7) / 8
|
|
2272
|
+
rows = [(raw.bytesize + stride - 1) / stride, height].min
|
|
2273
|
+
need = rows * stride
|
|
2274
|
+
raw = raw.byteslice(0, need) if raw.bytesize > need
|
|
2275
|
+
raw += "\0".b * (need - raw.bytesize) if raw.bytesize < need
|
|
2276
|
+
out = case bpc
|
|
2277
|
+
when 8 then translate(raw, lut)
|
|
2278
|
+
when 16
|
|
2279
|
+
buf = String.new(capacity: samples_per_row * rows, encoding: Encoding::BINARY)
|
|
2280
|
+
rows.times { |r| Pixels.high_bytes(raw, r * stride, samples_per_row, buf) }
|
|
2281
|
+
translate(buf, lut)
|
|
2282
|
+
when 2
|
|
2283
|
+
buf = String.new(capacity: (stride * 4) * rows, encoding: Encoding::BINARY)
|
|
2284
|
+
rows.times { |r| Pixels.two_bit(raw, r * stride, stride, buf, lut) }
|
|
2285
|
+
strip_padding(buf, stride * 4, samples_per_row, rows)
|
|
2286
|
+
else unpack(raw, samples_per_row, stride, rows, bpc, lut)
|
|
2287
|
+
end
|
|
2288
|
+
[out, rows]
|
|
2289
|
+
end
|
|
2290
|
+
|
|
2291
|
+
# 1- and 4-bit samples via unpack ("B"/"H") and String#tr.
|
|
2292
|
+
def unpack(raw, samples_per_row, stride, rows, bpc, lut)
|
|
2293
|
+
format = bpc == 1 ? "B" : "H"
|
|
2294
|
+
chars = bpc == 1 ? "01" : "0123456789abcdef"
|
|
2295
|
+
if (samples_per_row * bpc) % 8 == 0
|
|
2296
|
+
out = raw.unpack1("#{format}*")
|
|
2297
|
+
else
|
|
2298
|
+
out = String.new(capacity: samples_per_row * rows)
|
|
2299
|
+
directive = "#{format}#{samples_per_row}"
|
|
2300
|
+
rows.times { |r| out << raw.unpack1(directive, offset: r * stride) }
|
|
2301
|
+
end
|
|
2302
|
+
out.force_encoding(Encoding::BINARY)
|
|
2303
|
+
out.tr!(chars, Image::TR_ESCAPE.call(lut))
|
|
2304
|
+
out
|
|
2305
|
+
end
|
|
2306
|
+
|
|
2307
|
+
def strip_padding(buf, have, want, rows)
|
|
2308
|
+
return buf if have == want
|
|
2309
|
+
|
|
2310
|
+
out = String.new(capacity: want * rows, encoding: Encoding::BINARY)
|
|
2311
|
+
rows.times { |r| out << buf.byteslice(r * have, want) }
|
|
2312
|
+
out
|
|
2313
|
+
end
|
|
2314
|
+
|
|
2315
|
+
def translate(bytes, lut)
|
|
2316
|
+
return bytes if lut == IDENTITY
|
|
2317
|
+
|
|
2318
|
+
bytes.tr(ALL_BYTES, Image::TR_ESCAPE.call(lut))
|
|
2319
|
+
end
|
|
2320
|
+
|
|
2321
|
+
def convert(samples, width, rows, space, luts)
|
|
2322
|
+
out = String.new(capacity: width * rows, encoding: Encoding::BINARY)
|
|
2323
|
+
ncomp = space.components
|
|
2324
|
+
row = width * ncomp
|
|
2325
|
+
kind = space.kind
|
|
2326
|
+
rows.times do |r|
|
|
2327
|
+
off = r * row
|
|
2328
|
+
if luts then Pixels.mapped(samples, off, width, out, kind, luts)
|
|
2329
|
+
elsif kind == :rgb then Pixels.rgb(samples, off, width, out)
|
|
2330
|
+
elsif kind == :cmyk then Pixels.cmyk(samples, off, width, out)
|
|
2331
|
+
elsif kind == :lab then Pixels.first(samples, off, width, out, ncomp)
|
|
2332
|
+
else Pixels.device_n(samples, off, width, out, ncomp)
|
|
2333
|
+
end
|
|
2334
|
+
end
|
|
2335
|
+
out
|
|
2336
|
+
end
|
|
2337
|
+
|
|
2338
|
+
def dct_image(jpeg, width, height, space, decode)
|
|
2339
|
+
start = jpeg.index("\xFF\xD8".b) or raise ImageError, "DCTDecode data is not a JPEG"
|
|
2340
|
+
jpeg = jpeg.byteslice(start, jpeg.bytesize - start) if start.positive?
|
|
2341
|
+
# A frame much larger than the image dictionary says is corrupt
|
|
2342
|
+
# (and would be expensive to decode).
|
|
2343
|
+
jw, jh = JPEG.dimensions(jpeg)
|
|
2344
|
+
if jw * jh > 4 * width * height + 4096
|
|
2345
|
+
raise ImageError, "JPEG size #{jw}x#{jh} does not match the image (#{width}x#{height})"
|
|
2346
|
+
end
|
|
2347
|
+
|
|
2348
|
+
# Plain images (luminance = JPEG.decode) go undecoded with raw_jpeg.
|
|
2349
|
+
components = jpeg_components(jpeg) || space&.components || 1
|
|
2350
|
+
case components
|
|
2351
|
+
when 1
|
|
2352
|
+
lut = one_component(space).luma_lut(8, decode)
|
|
2353
|
+
return RawJpeg.new(jpeg, jw, jh) if @raw_jpeg && lut == IDENTITY
|
|
2354
|
+
|
|
2355
|
+
image = JPEG.decode(jpeg)
|
|
2356
|
+
lut == IDENTITY ? image : Image.new(image.width, image.height, translate(image.data, lut))
|
|
2357
|
+
when 4
|
|
2358
|
+
# JPEG.decode reads CMYK the Adobe way (stored inverted), which
|
|
2359
|
+
# PDFs declare with /Decode [1 0 1 0 1 0 1 0].
|
|
2360
|
+
cmyk = space&.components == 4 ? space : ColorSpace::CMYK
|
|
2361
|
+
raise UnsupportedImageError, "CMYK JPEG image without an inverting /Decode array" unless cmyk.inverted?(decode)
|
|
2362
|
+
|
|
2363
|
+
@raw_jpeg ? RawJpeg.new(jpeg, jw, jh) : JPEG.decode(jpeg)
|
|
2364
|
+
else
|
|
2365
|
+
rgb = space&.components == 3 ? space : ColorSpace::RGB
|
|
2366
|
+
invert = rgb.kind != :lab && rgb.inverted?(decode)
|
|
2367
|
+
return RawJpeg.new(jpeg, jw, jh) if @raw_jpeg && !invert && components == 3
|
|
2368
|
+
|
|
2369
|
+
image = JPEG.decode(jpeg)
|
|
2370
|
+
invert ? image.invert : image
|
|
2371
|
+
end
|
|
2372
|
+
end
|
|
2373
|
+
|
|
2374
|
+
# Number of components in the JPEG frame header.
|
|
2375
|
+
def jpeg_components(jpeg)
|
|
2376
|
+
pos = 2
|
|
2377
|
+
while pos + 10 <= jpeg.bytesize
|
|
2378
|
+
return nil unless jpeg.getbyte(pos) == 0xFF
|
|
2379
|
+
|
|
2380
|
+
marker = jpeg.getbyte(pos + 1)
|
|
2381
|
+
if marker == 0xFF || marker == 0x01 || marker.between?(0xD0, 0xD7)
|
|
2382
|
+
pos += marker == 0xFF ? 1 : 2
|
|
2383
|
+
next
|
|
2384
|
+
end
|
|
2385
|
+
return jpeg.getbyte(pos + 9) if SOF_MARKERS.include?(marker)
|
|
2386
|
+
|
|
2387
|
+
pos += 2 + ((jpeg.getbyte(pos + 2) << 8) | jpeg.getbyte(pos + 3))
|
|
2388
|
+
end
|
|
2389
|
+
nil
|
|
2390
|
+
end
|
|
2391
|
+
|
|
2392
|
+
def ccitt_image(data, params, width, height, space, decode)
|
|
2393
|
+
params ||= {}
|
|
2394
|
+
rows = positive(params[:Rows])
|
|
2395
|
+
image = CCITT.decode(
|
|
2396
|
+
data,
|
|
2397
|
+
columns: positive(params[:Columns]) || width,
|
|
2398
|
+
rows: rows ? [rows, height].min : height,
|
|
2399
|
+
k: int(params[:K]) || 0,
|
|
2400
|
+
black_is_1: params[:BlackIs1] == true,
|
|
2401
|
+
encoded_byte_align: params[:EncodedByteAlign] == true,
|
|
2402
|
+
end_of_line: params[:EndOfLine] == true
|
|
2403
|
+
)
|
|
2404
|
+
bilevel(image, space.luma_lut(1, decode), width, height)
|
|
2405
|
+
end
|
|
2406
|
+
|
|
2407
|
+
def jbig2_image(data, params, width, height, space, decode)
|
|
2408
|
+
globals = params && params[:JBIG2Globals]
|
|
2409
|
+
globals = @doc.decode_stream(globals) if globals.is_a?(Stream)
|
|
2410
|
+
image = JBIG2.decode(data, globals: globals.is_a?(String) ? globals : nil,
|
|
2411
|
+
max_pixels: 4 * width * height + 65_536)
|
|
2412
|
+
bilevel(image, space.luma_lut(1, decode), width, height)
|
|
2413
|
+
end
|
|
2414
|
+
|
|
2415
|
+
# A 0/255 bilevel image (0 = sample value 0) mapped through +lut+
|
|
2416
|
+
# and fitted to the declared size (missing parts white).
|
|
2417
|
+
def bilevel(image, lut, width, height)
|
|
2418
|
+
pixels = image.data
|
|
2419
|
+
if lut != [0, 255]
|
|
2420
|
+
pixels = pixels.tr("\x00\xFF".b, Image::TR_ESCAPE.call(lut))
|
|
2421
|
+
end
|
|
2422
|
+
return Image.new(width, height, pixels) if image.width == width && image.height == height
|
|
2423
|
+
|
|
2424
|
+
out = String.new(capacity: width * height, encoding: Encoding::BINARY)
|
|
2425
|
+
w = [image.width, width].min
|
|
2426
|
+
pad = "\xFF".b * (width - w)
|
|
2427
|
+
[image.height, height].min.times do |y|
|
|
2428
|
+
out << pixels.byteslice(y * image.width, w) << pad
|
|
2429
|
+
end
|
|
2430
|
+
out << ("\xFF".b * (width * height - out.bytesize))
|
|
2431
|
+
Image.new(width, height, out)
|
|
2432
|
+
end
|
|
2433
|
+
end
|
|
2434
|
+
|
|
2435
|
+
# JBIG2 decoder for PDF's embedded streams (ITU-T T.88): page
|
|
2436
|
+
# information, immediate generic regions (MQ arithmetic coding with
|
|
2437
|
+
# templates 0-3, adaptive pixels and typical prediction, or MMR) and
|
|
2438
|
+
# end-of-stripe segments, composed onto the page with any combination
|
|
2439
|
+
# operator. Text, halftone and refinement regions (symbol coding) raise
|
|
2440
|
+
# UnsupportedImageError: an image is decoded completely or not at all.
|
|
2441
|
+
module JBIG2
|
|
2442
|
+
# MQ coder probability states: [Qe, next MPS, next LPS, switch].
|
|
2443
|
+
QE = [
|
|
2444
|
+
[0x5601, 1, 1, 1], [0x3401, 2, 6, 0], [0x1801, 3, 9, 0], [0x0AC1, 4, 12, 0],
|
|
2445
|
+
[0x0521, 5, 29, 0], [0x0221, 38, 33, 0], [0x5601, 7, 6, 1], [0x5401, 8, 14, 0],
|
|
2446
|
+
[0x4801, 9, 14, 0], [0x3801, 10, 14, 0], [0x3001, 11, 17, 0], [0x2401, 12, 18, 0],
|
|
2447
|
+
[0x1C01, 13, 20, 0], [0x1601, 29, 21, 0], [0x5601, 15, 14, 1], [0x5401, 16, 14, 0],
|
|
2448
|
+
[0x5101, 17, 15, 0], [0x4801, 18, 16, 0], [0x3801, 19, 17, 0], [0x3401, 20, 18, 0],
|
|
2449
|
+
[0x3001, 21, 19, 0], [0x2801, 22, 19, 0], [0x2401, 23, 20, 0], [0x2201, 24, 21, 0],
|
|
2450
|
+
[0x1C01, 25, 22, 0], [0x1801, 26, 23, 0], [0x1601, 27, 24, 0], [0x1401, 28, 25, 0],
|
|
2451
|
+
[0x1201, 29, 26, 0], [0x1101, 30, 27, 0], [0x0AC1, 31, 28, 0], [0x09C1, 32, 29, 0],
|
|
2452
|
+
[0x08A1, 33, 30, 0], [0x0521, 34, 31, 0], [0x0441, 35, 32, 0], [0x02A1, 36, 33, 0],
|
|
2453
|
+
[0x0221, 37, 34, 0], [0x0141, 38, 35, 0], [0x0111, 39, 36, 0], [0x0085, 40, 37, 0],
|
|
2454
|
+
[0x0049, 41, 38, 0], [0x0025, 42, 39, 0], [0x0015, 43, 40, 0], [0x0009, 44, 41, 0],
|
|
2455
|
+
[0x0005, 45, 42, 0], [0x0001, 45, 43, 0], [0x5601, 46, 46, 0]
|
|
2456
|
+
].freeze
|
|
2457
|
+
# Per state: Qe, and the packed context (state << 1 | mps) after an
|
|
2458
|
+
# MPS or LPS decision, for mps 0 and 1.
|
|
2459
|
+
QE_VALUE = QE.map(&:first).freeze
|
|
2460
|
+
NEXT_MPS = QE.each_with_index.flat_map { |(_, nmps, _, _), _i| [nmps << 1, (nmps << 1) | 1] }.freeze
|
|
2461
|
+
NEXT_LPS = QE.flat_map do |_, _, nlps, switch|
|
|
2462
|
+
[(nlps << 1) | switch, (nlps << 1) | (1 - switch)]
|
|
2463
|
+
end.freeze
|
|
2464
|
+
|
|
2465
|
+
# Generic region templates (T.88 6.2.5.3) as sliding windows over
|
|
2466
|
+
# the rows y-2, y-1 and y: [row y-2 rightmost dx, its width, row
|
|
2467
|
+
# y-1 rightmost dx, its width, row y width], with the nominal
|
|
2468
|
+
# adaptive pixel positions inside the windows; the context bit of
|
|
2469
|
+
# each adaptive pixel; its nominal position; the typical prediction
|
|
2470
|
+
# context.
|
|
2471
|
+
TEMPLATES = [
|
|
2472
|
+
{ windows: [2, 5, 3, 7, 4], at_bits: [4, 10, 11, 15], at: [[3, -1], [-3, -1], [2, -2], [-2, -2]],
|
|
2473
|
+
sltp: 0x9B25 },
|
|
2474
|
+
{ windows: [2, 4, 3, 6, 3], at_bits: [3], at: [[3, -1]], sltp: 0x0795 },
|
|
2475
|
+
{ windows: [1, 3, 2, 5, 2], at_bits: [2], at: [[2, -1]], sltp: 0x00E5 },
|
|
2476
|
+
{ windows: [0, 0, 2, 6, 4], at_bits: [4], at: [[2, -1]], sltp: 0x0195 }
|
|
2477
|
+
].freeze
|
|
2478
|
+
PAD = 4 # zero pixels left and right of each bitmap row
|
|
2479
|
+
BITS = "\x00\x01".b.freeze # bitmap: 1 = black
|
|
2480
|
+
LUMA = "\xFF\x00".b.freeze
|
|
2481
|
+
|
|
2482
|
+
UNSUPPORTED = {
|
|
2483
|
+
4 => "text region", 6 => "text region", 7 => "text region", 20 => "halftone region",
|
|
2484
|
+
22 => "halftone region", 23 => "halftone region", 40 => "refinement region",
|
|
2485
|
+
42 => "refinement region", 43 => "refinement region"
|
|
2486
|
+
}.freeze
|
|
2487
|
+
|
|
2488
|
+
# Decodes an embedded JBIG2 stream (+globals+: the JBIG2Globals
|
|
2489
|
+
# data) to an Image (black 0, white 255). Pages and regions larger
|
|
2490
|
+
# than +max_pixels+ are rejected.
|
|
2491
|
+
def self.decode(data, globals: nil, max_pixels: MAX_PIXELS)
|
|
2492
|
+
Page.new(max_pixels).decode(globals, data)
|
|
2493
|
+
end
|
|
2494
|
+
|
|
2495
|
+
# Segment parsing and page composition.
|
|
2496
|
+
class Page
|
|
2497
|
+
include Util
|
|
2498
|
+
|
|
2499
|
+
def initialize(max_pixels)
|
|
2500
|
+
@max_pixels = max_pixels
|
|
2501
|
+
end
|
|
2502
|
+
|
|
2503
|
+
def decode(globals, data)
|
|
2504
|
+
each_segment(globals) { |type, _body| raise_unsupported(type) } if globals
|
|
2505
|
+
each_segment(data) do |type, body|
|
|
2506
|
+
case type
|
|
2507
|
+
when 48 then page_information(body)
|
|
2508
|
+
when 38, 39 then generic_region(body)
|
|
2509
|
+
when 50 then end_of_stripe(body)
|
|
2510
|
+
when 49, 51 then break
|
|
2511
|
+
else raise_unsupported(type)
|
|
2512
|
+
end
|
|
2513
|
+
end
|
|
2514
|
+
raise ImageError, "JBIG2 data without a page" unless @page
|
|
2515
|
+
|
|
2516
|
+
height = @height || @rows
|
|
2517
|
+
raise ImageError, "empty JBIG2 page" unless height.positive?
|
|
2518
|
+
|
|
2519
|
+
bitmap = @page.byteslice(0, @width * height)
|
|
2520
|
+
Image.new(@width, height, bitmap.tr(BITS, LUMA))
|
|
2521
|
+
end
|
|
2522
|
+
|
|
2523
|
+
private
|
|
2524
|
+
|
|
2525
|
+
def raise_unsupported(type)
|
|
2526
|
+
what = UNSUPPORTED[type] or return
|
|
2527
|
+
raise UnsupportedImageError, "JBIG2 images with symbol coding (#{what}) are not supported"
|
|
2528
|
+
end
|
|
2529
|
+
|
|
2530
|
+
# Yields [type, data] of each segment (T.88 7.2).
|
|
2531
|
+
def each_segment(data)
|
|
2532
|
+
pos = 0
|
|
2533
|
+
size = data.bytesize
|
|
2534
|
+
while pos + 11 <= size
|
|
2535
|
+
number = be_int(data, pos, 4)
|
|
2536
|
+
flags = data.getbyte(pos + 4)
|
|
2537
|
+
type = flags & 0x3F
|
|
2538
|
+
pos += 5
|
|
2539
|
+
count = data.getbyte(pos) >> 5
|
|
2540
|
+
if count == 7
|
|
2541
|
+
return if pos + 4 > size
|
|
2542
|
+
|
|
2543
|
+
count = be_int(data, pos, 4) & 0x1FFFFFFF
|
|
2544
|
+
pos += 4 + (count + 8) / 8
|
|
2545
|
+
else
|
|
2546
|
+
pos += 1
|
|
2547
|
+
end
|
|
2548
|
+
pos += count * (number <= 256 ? 1 : number <= 65_536 ? 2 : 4)
|
|
2549
|
+
pos += flags & 0x40 == 0 ? 1 : 4
|
|
2550
|
+
return if pos + 4 > size
|
|
2551
|
+
|
|
2552
|
+
length = be_int(data, pos, 4)
|
|
2553
|
+
pos += 4
|
|
2554
|
+
length = unknown_length(data, pos) if length == 0xFFFFFFFF
|
|
2555
|
+
length = size - pos if pos + length > size
|
|
2556
|
+
yield type, data.byteslice(pos, length)
|
|
2557
|
+
pos += length
|
|
2558
|
+
end
|
|
2559
|
+
end
|
|
2560
|
+
|
|
2561
|
+
# Data length of an immediate generic region of unknown size:
|
|
2562
|
+
# up to its end marker and row count (T.88 7.2.7).
|
|
2563
|
+
def unknown_length(data, pos)
|
|
2564
|
+
return data.bytesize - pos if pos + 18 > data.bytesize
|
|
2565
|
+
|
|
2566
|
+
mmr = data.getbyte(pos + 17) & 1 == 1
|
|
2567
|
+
marker = mmr ? "\x00\x00".b : "\xFF\xAC".b
|
|
2568
|
+
stop = data.index(marker, pos + 18) or return data.bytesize - pos
|
|
2569
|
+
stop + 6 - pos
|
|
2570
|
+
end
|
|
2571
|
+
|
|
2572
|
+
def page_information(body)
|
|
2573
|
+
raise ImageError, "short JBIG2 page information" if body.bytesize < 19
|
|
2574
|
+
|
|
2575
|
+
width = be_int(body, 0, 4)
|
|
2576
|
+
height = be_int(body, 4, 4)
|
|
2577
|
+
flags = body.getbyte(16)
|
|
2578
|
+
@default = flags[2]
|
|
2579
|
+
@operator = (flags >> 3) & 3
|
|
2580
|
+
@override = flags[6] == 1
|
|
2581
|
+
@height = height == 0xFFFFFFFF ? nil : height
|
|
2582
|
+
@width = width
|
|
2583
|
+
rows = @height || 0 # striped pages of unknown height grow
|
|
2584
|
+
raise ImageError, "invalid JBIG2 page size #{width}x#{height}" unless width.positive?
|
|
2585
|
+
raise UnsupportedImageError, "JBIG2 page too large" if width * [rows, 1].max > @max_pixels
|
|
2586
|
+
|
|
2587
|
+
@rows = rows
|
|
2588
|
+
@page = @default.chr.b * (width * rows)
|
|
2589
|
+
end
|
|
2590
|
+
|
|
2591
|
+
def end_of_stripe(body)
|
|
2592
|
+
return unless @page && @height.nil? && body.bytesize >= 4
|
|
2593
|
+
|
|
2594
|
+
grow(be_int(body, 0, 4) + 1)
|
|
2595
|
+
end
|
|
2596
|
+
|
|
2597
|
+
# Pages of unknown height grow with their stripes.
|
|
2598
|
+
def grow(rows)
|
|
2599
|
+
return if rows <= @rows
|
|
2600
|
+
raise UnsupportedImageError, "JBIG2 page too large" if @width * rows > @max_pixels
|
|
2601
|
+
|
|
2602
|
+
@page << (@default.chr.b * (@width * (rows - @rows)))
|
|
2603
|
+
@rows = rows
|
|
2604
|
+
end
|
|
2605
|
+
|
|
2606
|
+
def generic_region(body)
|
|
2607
|
+
raise ImageError, "JBIG2 region before page information" unless @page
|
|
2608
|
+
raise ImageError, "short JBIG2 generic region" if body.bytesize < 18
|
|
2609
|
+
|
|
2610
|
+
width, height, x, y = body.unpack("N4")
|
|
2611
|
+
x -= 1 << 32 if x >= 1 << 31
|
|
2612
|
+
y -= 1 << 32 if y >= 1 << 31
|
|
2613
|
+
operator = @override ? body.getbyte(16) & 7 : @operator
|
|
2614
|
+
flags = body.getbyte(17)
|
|
2615
|
+
raise UnsupportedImageError, "JBIG2 extended templates are not supported" if flags & 0x10 != 0
|
|
2616
|
+
|
|
2617
|
+
height = be_int(body, body.bytesize - 4, 4) if height == 0xFFFFFFFF && body.bytesize >= 22
|
|
2618
|
+
unless width.positive? && height.positive? && width * height <= @max_pixels
|
|
2619
|
+
raise ImageError, "invalid JBIG2 region size #{width}x#{height}"
|
|
2620
|
+
end
|
|
2621
|
+
|
|
2622
|
+
bitmap = if flags & 1 == 1
|
|
2623
|
+
mmr(body.byteslice(18, body.bytesize - 18), width, height)
|
|
2624
|
+
else
|
|
2625
|
+
template = (flags >> 1) & 3
|
|
2626
|
+
at_count = template.zero? ? 4 : 1
|
|
2627
|
+
at = body.byteslice(18, 2 * at_count).unpack("c*").each_slice(2).to_a
|
|
2628
|
+
raise ImageError, "short JBIG2 generic region" if at.size < at_count
|
|
2629
|
+
|
|
2630
|
+
data = body.byteslice(18 + 2 * at_count, body.bytesize)
|
|
2631
|
+
GenericRegion.new(data, width, height, template, at, flags[3] == 1).decode
|
|
2632
|
+
end
|
|
2633
|
+
grow(y + height) if @height.nil?
|
|
2634
|
+
compose(bitmap, width, height, x, y, operator)
|
|
2635
|
+
end
|
|
2636
|
+
|
|
2637
|
+
# MMR-coded generic region: T.6 with 1 = black.
|
|
2638
|
+
def mmr(data, width, height)
|
|
2639
|
+
image = CCITT.decode(data, columns: width, rows: height, k: -1)
|
|
2640
|
+
bits = image.data.tr(LUMA, BITS)
|
|
2641
|
+
bits << ("\0".b * (width * (height - image.height))) if image.height < height
|
|
2642
|
+
bits
|
|
2643
|
+
end
|
|
2644
|
+
|
|
2645
|
+
def compose(bitmap, width, height, x, y, operator)
|
|
2646
|
+
page = @page
|
|
2647
|
+
pw = @width
|
|
2648
|
+
x0 = [x, 0].max
|
|
2649
|
+
x1 = [x + width, pw].min
|
|
2650
|
+
n = x1 - x0
|
|
2651
|
+
return if n <= 0
|
|
2652
|
+
|
|
2653
|
+
height.times do |ry|
|
|
2654
|
+
py = y + ry
|
|
2655
|
+
next if py.negative?
|
|
2656
|
+
break if py >= @rows
|
|
2657
|
+
|
|
2658
|
+
src = ry * width + (x0 - x)
|
|
2659
|
+
dst = py * pw + x0
|
|
2660
|
+
piece = bitmap.byteslice(src, n)
|
|
2661
|
+
if operator == 4 || (operator.zero? && !page.byteslice(dst, n).include?("\x01"))
|
|
2662
|
+
page.bytesplice(dst, n, piece)
|
|
2663
|
+
elsif operator != 0 || piece.include?("\x01")
|
|
2664
|
+
combine(page, dst, piece, n, operator)
|
|
2665
|
+
end
|
|
2666
|
+
end
|
|
2667
|
+
end
|
|
2668
|
+
|
|
2669
|
+
def combine(page, dst, piece, n, operator)
|
|
2670
|
+
i = 0
|
|
2671
|
+
while i < n
|
|
2672
|
+
a = page.getbyte(dst + i)
|
|
2673
|
+
b = piece.getbyte(i)
|
|
2674
|
+
v = case operator
|
|
2675
|
+
when 0 then a | b
|
|
2676
|
+
when 1 then a & b
|
|
2677
|
+
when 2 then a ^ b
|
|
2678
|
+
else 1 ^ a ^ b
|
|
2679
|
+
end
|
|
2680
|
+
page.setbyte(dst + i, v)
|
|
2681
|
+
i += 1
|
|
2682
|
+
end
|
|
2683
|
+
end
|
|
2684
|
+
end
|
|
2685
|
+
|
|
2686
|
+
# Arithmetic-coded generic region decoding (T.88 6.2.5.7) with the
|
|
2687
|
+
# MQ decoder of Annex E. The bitmap has PAD zero pixels left and
|
|
2688
|
+
# right of each row and two zero rows on top, so that the context
|
|
2689
|
+
# windows never need bounds checks. Past the end of its data the
|
|
2690
|
+
# decoder reads 1 bits; a valid stream needs only a few of them, so
|
|
2691
|
+
# decoding stops (the rest stays white) after FEED_LIMIT.
|
|
2692
|
+
class GenericRegion
|
|
2693
|
+
FEED_LIMIT = 64
|
|
2694
|
+
|
|
2695
|
+
def initialize(data, width, height, template, at, typical)
|
|
2696
|
+
@data = data
|
|
2697
|
+
@size = data.bytesize
|
|
2698
|
+
@width = width
|
|
2699
|
+
@height = height
|
|
2700
|
+
@template = TEMPLATES[template]
|
|
2701
|
+
@at = at
|
|
2702
|
+
@typical = typical
|
|
2703
|
+
@contexts = Array.new(1 << 16, 0)
|
|
2704
|
+
nominal = @template[:at]
|
|
2705
|
+
@patches = at.each_with_index.filter_map do |(dx, dy), i|
|
|
2706
|
+
next if [dx, dy] == nominal[i]
|
|
2707
|
+
raise ImageError, "invalid JBIG2 adaptive pixel" if dy.positive? || (dy.zero? && dx >= 0)
|
|
2708
|
+
|
|
2709
|
+
[dx, dy, @template[:at_bits][i]]
|
|
2710
|
+
end
|
|
2711
|
+
@stride = width + 2 * PAD
|
|
2712
|
+
init_decoder
|
|
2713
|
+
end
|
|
2714
|
+
|
|
2715
|
+
def decode
|
|
2716
|
+
stride = @stride
|
|
2717
|
+
buf = "\0".b * (stride * (@height + 2))
|
|
2718
|
+
ltp = 0
|
|
2719
|
+
sltp = @template[:sltp]
|
|
2720
|
+
@height.times do |y|
|
|
2721
|
+
break if @fed > FEED_LIMIT
|
|
2722
|
+
|
|
2723
|
+
row = (y + 2) * stride + PAD
|
|
2724
|
+
if @typical
|
|
2725
|
+
ltp ^= decode_bit(sltp)
|
|
2726
|
+
if ltp == 1
|
|
2727
|
+
buf.bytesplice(row - PAD, stride, buf.byteslice(row - PAD - stride, stride))
|
|
2728
|
+
next
|
|
2729
|
+
end
|
|
2730
|
+
end
|
|
2731
|
+
decode_row(buf, row, y)
|
|
2732
|
+
end
|
|
2733
|
+
out = String.new(capacity: @width * @height, encoding: Encoding::BINARY)
|
|
2734
|
+
@height.times { |y| out << buf.byteslice((y + 2) * stride + PAD, @width) }
|
|
2735
|
+
out
|
|
2736
|
+
end
|
|
2737
|
+
|
|
2738
|
+
private
|
|
2739
|
+
|
|
2740
|
+
def init_decoder
|
|
2741
|
+
@fed = 0
|
|
2742
|
+
@bp = 0
|
|
2743
|
+
@c = (@data.getbyte(0) || 0xFF) << 16
|
|
2744
|
+
byte_in
|
|
2745
|
+
@c = (@c << 7) & 0xFFFFFFFF
|
|
2746
|
+
@ct -= 7
|
|
2747
|
+
@a = 0x8000
|
|
2748
|
+
end
|
|
2749
|
+
|
|
2750
|
+
def byte_in
|
|
2751
|
+
data = @data
|
|
2752
|
+
bp = @bp
|
|
2753
|
+
if data.getbyte(bp) == 0xFF
|
|
2754
|
+
b1 = data.getbyte(bp + 1)
|
|
2755
|
+
if b1.nil? || b1 > 0x8F
|
|
2756
|
+
@c += 0xFF00
|
|
2757
|
+
@ct = 8
|
|
2758
|
+
@fed += 1
|
|
2759
|
+
else
|
|
2760
|
+
@bp = bp + 1
|
|
2761
|
+
@c += b1 << 9
|
|
2762
|
+
@ct = 7
|
|
2763
|
+
end
|
|
2764
|
+
else
|
|
2765
|
+
@bp = bp += 1
|
|
2766
|
+
if bp < @size
|
|
2767
|
+
@c += data.getbyte(bp) << 8
|
|
2768
|
+
else
|
|
2769
|
+
@c += 0xFF00
|
|
2770
|
+
@fed += 1
|
|
2771
|
+
end
|
|
2772
|
+
@ct = 8
|
|
2773
|
+
end
|
|
2774
|
+
end
|
|
2775
|
+
|
|
2776
|
+
# One decision with context +cx+ (used for typical prediction).
|
|
2777
|
+
def decode_bit(cx)
|
|
2778
|
+
contexts = @contexts
|
|
2779
|
+
state = contexts[cx]
|
|
2780
|
+
qe = QE_VALUE[state >> 1]
|
|
2781
|
+
mps = state & 1
|
|
2782
|
+
a = @a - qe
|
|
2783
|
+
if (@c >> 16) < qe
|
|
2784
|
+
if a < qe
|
|
2785
|
+
bit = mps
|
|
2786
|
+
contexts[cx] = NEXT_MPS[state]
|
|
2787
|
+
else
|
|
2788
|
+
bit = 1 - mps
|
|
2789
|
+
contexts[cx] = NEXT_LPS[state]
|
|
2790
|
+
end
|
|
2791
|
+
a = qe
|
|
2792
|
+
else
|
|
2793
|
+
@c -= qe << 16
|
|
2794
|
+
if a & 0x8000 != 0
|
|
2795
|
+
@a = a
|
|
2796
|
+
return mps
|
|
2797
|
+
end
|
|
2798
|
+
if a < qe
|
|
2799
|
+
bit = 1 - mps
|
|
2800
|
+
contexts[cx] = NEXT_LPS[state]
|
|
2801
|
+
else
|
|
2802
|
+
bit = mps
|
|
2803
|
+
contexts[cx] = NEXT_MPS[state]
|
|
2804
|
+
end
|
|
2805
|
+
end
|
|
2806
|
+
loop do
|
|
2807
|
+
byte_in if @ct.zero?
|
|
2808
|
+
a <<= 1
|
|
2809
|
+
@c = (@c << 1) & 0xFFFFFFFF
|
|
2810
|
+
@ct -= 1
|
|
2811
|
+
break if a & 0x8000 != 0
|
|
2812
|
+
end
|
|
2813
|
+
@a = a
|
|
2814
|
+
bit
|
|
2815
|
+
end
|
|
2816
|
+
|
|
2817
|
+
# Decodes one row at +row+ (the offset of pixel 0) of +buf+. The
|
|
2818
|
+
# context is built from sliding windows over the rows y-2, y-1
|
|
2819
|
+
# and y; adaptive pixels off their nominal positions are patched
|
|
2820
|
+
# in. The MQ decoder is inlined (this loop runs per pixel).
|
|
2821
|
+
def decode_row(buf, row, y)
|
|
2822
|
+
hi2, n2, hi1, n1, n0 = @template[:windows]
|
|
2823
|
+
m0 = (1 << n0) - 1
|
|
2824
|
+
s1 = n0
|
|
2825
|
+
s2 = n0 + n1
|
|
2826
|
+
r2 = row - 2 * @stride + hi2 + 1 # next pixel to enter each window
|
|
2827
|
+
r1 = row - @stride + hi1 + 1
|
|
2828
|
+
# Rows y-2 and y-1 share one window: shifting moves row y-1's
|
|
2829
|
+
# oldest pixel onto row y-2's newest bit, which the mask clears.
|
|
2830
|
+
mask = ((((1 << n2) - 1) << s2) | (((1 << n1) - 1) << s1)) & ~(1 << s1) & ~(1 << s2)
|
|
2831
|
+
above = 0
|
|
2832
|
+
(hi2 - n2 + 1).upto(hi2) { |dx| above |= buf.getbyte(r2 - hi2 - 1 + dx) << (s2 + hi2 - dx) }
|
|
2833
|
+
(hi1 - n1 + 1).upto(hi1) { |dx| above |= buf.getbyte(r1 - hi1 - 1 + dx) << (s1 + hi1 - dx) }
|
|
2834
|
+
row2 = n2.positive?
|
|
2835
|
+
w0 = 0
|
|
2836
|
+
patches = @patches.empty? ? nil : @patches
|
|
2837
|
+
contexts = @contexts
|
|
2838
|
+
qe_value = QE_VALUE
|
|
2839
|
+
next_mps = NEXT_MPS
|
|
2840
|
+
next_lps = NEXT_LPS
|
|
2841
|
+
data = @data
|
|
2842
|
+
size = @size
|
|
2843
|
+
a = @a
|
|
2844
|
+
c = @c
|
|
2845
|
+
ct = @ct
|
|
2846
|
+
bp = @bp
|
|
2847
|
+
fed = @fed
|
|
2848
|
+
width = @width
|
|
2849
|
+
x = 0
|
|
2850
|
+
while x < width
|
|
2851
|
+
cx = above | w0
|
|
2852
|
+
cx = patch(cx, patches, buf, x, y) if patches
|
|
2853
|
+
state = contexts[cx]
|
|
2854
|
+
qe = qe_value[state >> 1]
|
|
2855
|
+
bit = state & 1
|
|
2856
|
+
a -= qe
|
|
2857
|
+
if (c >> 16) < qe
|
|
2858
|
+
if a < qe
|
|
2859
|
+
contexts[cx] = next_mps[state]
|
|
2860
|
+
else
|
|
2861
|
+
bit = 1 - bit
|
|
2862
|
+
contexts[cx] = next_lps[state]
|
|
2863
|
+
end
|
|
2864
|
+
a = qe
|
|
2865
|
+
else
|
|
2866
|
+
c -= qe << 16
|
|
2867
|
+
if a & 0x8000 == 0
|
|
2868
|
+
if a < qe
|
|
2869
|
+
bit = 1 - bit
|
|
2870
|
+
contexts[cx] = next_lps[state]
|
|
2871
|
+
else
|
|
2872
|
+
contexts[cx] = next_mps[state]
|
|
2873
|
+
end
|
|
2874
|
+
end
|
|
2875
|
+
end
|
|
2876
|
+
while a & 0x8000 == 0
|
|
2877
|
+
if ct.zero?
|
|
2878
|
+
if data.getbyte(bp) == 0xFF
|
|
2879
|
+
b1 = data.getbyte(bp + 1)
|
|
2880
|
+
if b1.nil? || b1 > 0x8F
|
|
2881
|
+
c += 0xFF00
|
|
2882
|
+
ct = 8
|
|
2883
|
+
fed += 1
|
|
2884
|
+
else
|
|
2885
|
+
bp += 1
|
|
2886
|
+
c += b1 << 9
|
|
2887
|
+
ct = 7
|
|
2888
|
+
end
|
|
2889
|
+
else
|
|
2890
|
+
bp += 1
|
|
2891
|
+
if bp < size
|
|
2892
|
+
c += data.getbyte(bp) << 8
|
|
2893
|
+
else
|
|
2894
|
+
c += 0xFF00
|
|
2895
|
+
fed += 1
|
|
2896
|
+
end
|
|
2897
|
+
ct = 8
|
|
2898
|
+
end
|
|
2899
|
+
end
|
|
2900
|
+
a <<= 1
|
|
2901
|
+
c = (c << 1) & 0xFFFFFFFF
|
|
2902
|
+
ct -= 1
|
|
2903
|
+
end
|
|
2904
|
+
buf.setbyte(row + x, 1) if bit == 1
|
|
2905
|
+
w0 = ((w0 << 1) | bit) & m0
|
|
2906
|
+
above = ((above << 1) & mask) | (buf.getbyte(r1 + x) << s1)
|
|
2907
|
+
above |= buf.getbyte(r2 + x) << s2 if row2
|
|
2908
|
+
x += 1
|
|
2909
|
+
end
|
|
2910
|
+
@a = a
|
|
2911
|
+
@c = c
|
|
2912
|
+
@ct = ct
|
|
2913
|
+
@bp = bp
|
|
2914
|
+
@fed = fed
|
|
2915
|
+
end
|
|
2916
|
+
|
|
2917
|
+
# Replaces the nominal adaptive pixels in +cx+ by the real ones.
|
|
2918
|
+
def patch(cx, patches, buf, x, y)
|
|
2919
|
+
patches.each do |dx, dy, bit|
|
|
2920
|
+
px = x + dx
|
|
2921
|
+
py = y + dy
|
|
2922
|
+
v = px >= 0 && px < @width && py >= 0 ? buf.getbyte((py + 2) * @stride + PAD + px) : 0
|
|
2923
|
+
cx = (cx & ~(1 << bit)) | (v << bit)
|
|
2924
|
+
end
|
|
2925
|
+
cx
|
|
2926
|
+
end
|
|
2927
|
+
end
|
|
2928
|
+
end
|
|
2929
|
+
|
|
2930
|
+
private_constant :ParseError, :Ref, :Keyword, :Stream, :Page, :ImageSource, :Band, :STREAM_LIMIT, :SAMPLE_LIMIT,
|
|
2931
|
+
:INTERNAL_ERRORS, :Util, :Parser, :Document, :Security, :Filters, :ColorSpace, :Pixels,
|
|
2932
|
+
:ContentScanner, :ImageDecoder, :JBIG2
|
|
2933
|
+
end
|
|
2934
|
+
end
|
|
2935
|
+
end
|