qr_reader 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +18 -0
- data/README.md +23 -7
- data/exe/qr_reader +1 -1
- data/lib/qr_reader/formats/jpeg.rb +61 -2
- data/lib/qr_reader/formats/jpeg2000/codestream.rb +447 -0
- data/lib/qr_reader/formats/jpeg2000/container.rb +156 -0
- data/lib/qr_reader/formats/jpeg2000/dwt.rb +607 -0
- data/lib/qr_reader/formats/jpeg2000/tier1.rb +1084 -0
- data/lib/qr_reader/formats/jpeg2000/tile.rb +722 -0
- data/lib/qr_reader/formats/jpeg2000.rb +764 -0
- data/lib/qr_reader/formats/pdf/font_programs.rb +1205 -0
- data/lib/qr_reader/formats/pdf/font_tables.rb +224 -0
- data/lib/qr_reader/formats/pdf/fonts.rb +412 -0
- data/lib/qr_reader/formats/pdf/text.rb +151 -0
- data/lib/qr_reader/formats/pdf/vector.rb +799 -0
- data/lib/qr_reader/formats/pdf.rb +183 -44
- data/lib/qr_reader/formats.rb +13 -4
- data/lib/qr_reader/reader.rb +36 -10
- data/lib/qr_reader/version.rb +1 -1
- data/lib/qr_reader.rb +1 -1
- metadata +15 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 676cc0ecf84f68ef07fc1b05b6c473c301eb4b82ee8ab615dc30bb036030834f
|
|
4
|
+
data.tar.gz: c7b893b1828be74bb1f4feed942b325b34b8183dc7c318919fb891f84b64b24d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '095f7dead62ad0f612d662155e30f2ff11b8b5120d8e74114f8ea55fca78286e5609994794e69b596f055b1c52cb101607ad36ad1434536505428e48c94ee2a1'
|
|
7
|
+
data.tar.gz: f9b0da7897ad79f9162f1e16fafc592d36421dab0521354110685a9f9e11a57720ec8e9173915b969f023d6c9a37fdd2cf68f14db018c3c304b0f5c552540304
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.0.2] - 2026-10-13
|
|
6
|
+
|
|
7
|
+
- JPEG 2000: pure-Ruby decoder for all of Part 1 (`Formats::JPEG2000`), used
|
|
8
|
+
for standalone JP2/J2K files and JPXDecode images in PDFs; large images are
|
|
9
|
+
read from a reduced-resolution decode first.
|
|
10
|
+
- PDF: QR codes drawn as vector graphics are read (groups of small filled or
|
|
11
|
+
stroked right-angled paths or round dots are rasterized;
|
|
12
|
+
`PDF.each_image(vector: true)`), also when set as text in an embedded
|
|
13
|
+
barcode font (TrueType, OpenType, CFF, Type 1, Type 3; simple and Type 0).
|
|
14
|
+
- PDF: CMYK and YCCK JPEGs without an inverting `/Decode` array are read
|
|
15
|
+
(as ink amounts, like other PDF readers) instead of being rejected;
|
|
16
|
+
`JPEG.decode(bytes, cmyk: :plain)`.
|
|
17
|
+
- PDF: page images cut into strips that overlap by a row or two are joined
|
|
18
|
+
again (a QR-bill printed through a driver that repeats a row per strip was
|
|
19
|
+
not found).
|
|
20
|
+
- Long thin images (e.g. 4961x17 strips) no longer raise
|
|
21
|
+
`ArgumentError: downscale factor too large`.
|
|
22
|
+
|
|
5
23
|
## [0.0.1] - 2026-10-13
|
|
6
24
|
|
|
7
25
|
- Initial release: pure-Ruby QR code reader (Model 2, versions 1–40; numeric,
|
data/README.md
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
A fast, accurate QR code reader in pure Ruby. No native extensions, no
|
|
4
4
|
ImageMagick, no zbar — just Ruby (and its standard library). Reads photos and
|
|
5
|
-
scanned documents: PNG, JPEG, GIF, BMP, PNM, TIFF (including CCITT
|
|
6
|
-
compression) and
|
|
5
|
+
scanned documents: PNG, JPEG, JPEG 2000, GIF, BMP, PNM, TIFF (including CCITT
|
|
6
|
+
G3/G4 fax compression) and PDFs (embedded images, vector-drawn codes and
|
|
7
|
+
barcode fonts).
|
|
7
8
|
|
|
8
9
|
```ruby
|
|
9
10
|
require "qr_reader"
|
|
@@ -138,13 +139,28 @@ What does help with big inputs:
|
|
|
138
139
|
150–600 DPI, fax pages (CCITT G3/G4, non-square 204x98 dpi pixels),
|
|
139
140
|
B&W/dithered scanner modes, multi-page TIFF and PDF, Swiss QR-bills (the
|
|
140
141
|
cross in the middle is just damage the error correction repairs).
|
|
141
|
-
-
|
|
142
|
-
|
|
143
|
-
|
|
142
|
+
- JPEG 2000 (JP2 files and raw codestreams, all of Part 1: tiles, layers,
|
|
143
|
+
precincts, every progression order and code-block style, 5/3 and 9/7
|
|
144
|
+
wavelets); large images are first decoded at reduced resolution.
|
|
145
|
+
- Scanned PDFs: images compressed with JPEG (also CMYK/YCCK), JPEG 2000, CCITT
|
|
146
|
+
fax, JBIG2 (generic regions), Flate, LZW, run-length; xref and object
|
|
147
|
+
streams; PDFs encrypted
|
|
148
|
+
with an empty user password; page images that printer drivers cut into
|
|
149
|
+
(overlapping) strips.
|
|
150
|
+
- Digitally created PDFs: QR codes drawn as vector graphics (rectangles or
|
|
151
|
+
other straight-edged paths per module or run of modules, round dots or
|
|
152
|
+
rounded modules, filled or stroked, also light on a coloured background)
|
|
153
|
+
and codes set as text in a barcode font (embedded TrueType, OpenType, CFF,
|
|
154
|
+
Type 1 or Type 3 fonts, simple or composite).
|
|
144
155
|
- Multiple codes per image (`decode_all` / `read_all`).
|
|
145
156
|
|
|
146
|
-
|
|
147
|
-
|
|
157
|
+
PDF pages are not rendered as a whole: qr_reader reads the raster images a page
|
|
158
|
+
draws, and renders only groups of small shapes (paths and barcode-font glyphs)
|
|
159
|
+
that look like a vector-drawn code. Codes drawn as shadings or patterns, in
|
|
160
|
+
fonts that are not embedded, or with glyphs that have curves are not found.
|
|
161
|
+
JPEG 2000 decoding is the slowest format (a 2900 x 3900 colour page takes about
|
|
162
|
+
15 s without YJIT; single-tile images above 32 megapixels are decoded at half
|
|
163
|
+
resolution).
|
|
148
164
|
Images larger than 67 megapixels (JPEG) or 134 megapixels (other formats) are
|
|
149
165
|
rejected rather than risking huge memory use.
|
|
150
166
|
|
data/exe/qr_reader
CHANGED
|
@@ -8,7 +8,7 @@ require_relative "../lib/qr_reader"
|
|
|
8
8
|
options = { all: false, format: :text, try_harder: true, inverted: :auto }
|
|
9
9
|
parser = OptionParser.new do |o|
|
|
10
10
|
o.banner = "Usage: qr_reader [options] IMAGE...\n" \
|
|
11
|
-
"Decodes QR codes in PNG, JPEG, GIF, BMP and
|
|
11
|
+
"Decodes QR codes in PNG, JPEG, JPEG 2000, GIF, BMP, PNM, TIFF and PDF files (use - for stdin)."
|
|
12
12
|
o.on("-a", "--all", "Print every code found (default: first code only)") { options[:all] = true }
|
|
13
13
|
o.on("-j", "--json", "Print results as JSON lines with metadata") { options[:format] = :json }
|
|
14
14
|
o.on("-v", "--verbose", "Print metadata (version, EC level, position) after the text") { options[:format] = :verbose }
|
|
@@ -46,10 +46,18 @@ module QrReader
|
|
|
46
46
|
class << self
|
|
47
47
|
# Decodes JPEG +bytes+ to a luminance Image. +scale+ (1, 2, 4 or 8)
|
|
48
48
|
# decodes at reduced size: ceil(width / scale) x ceil(height / scale).
|
|
49
|
-
|
|
49
|
+
# +cmyk+ says how 4-component (CMYK and YCCK) samples are read:
|
|
50
|
+
# :inverted (255 = no ink, as Adobe applications write them and as
|
|
51
|
+
# standalone CMYK JPEGs are read) or :plain (0 = no ink, as PDFs
|
|
52
|
+
# declare them without an inverting /Decode array).
|
|
53
|
+
def decode(bytes, scale: 1, cmyk: :inverted)
|
|
50
54
|
scale = check_scale(scale)
|
|
51
55
|
bytes = binary(bytes)
|
|
52
|
-
data =
|
|
56
|
+
data = case cmyk
|
|
57
|
+
when :inverted then guard("data") { QrReaderKernels.jpeg_luma(bytes, scale) }
|
|
58
|
+
when :plain then guard("data") { PlainCmykDecoder.new(bytes, scale, false).decode }
|
|
59
|
+
else raise ArgumentError, "cmyk must be :inverted or :plain, not #{cmyk.inspect}"
|
|
60
|
+
end
|
|
53
61
|
image(bytes, scale, data)
|
|
54
62
|
end
|
|
55
63
|
|
|
@@ -117,6 +125,57 @@ module QrReader
|
|
|
117
125
|
end
|
|
118
126
|
end
|
|
119
127
|
|
|
128
|
+
# The kernel's decoder with 4-component samples read as plain ink
|
|
129
|
+
# amounts (JPEG.decode with cmyk: :plain): the colour conversion of
|
|
130
|
+
# QrReaderKernels::JpegDecoder with every C, M, Y and K value v taken
|
|
131
|
+
# as 255 - v. Pure Ruby only (not a kernel entry point).
|
|
132
|
+
class PlainCmykDecoder < QrReaderKernels::JpegDecoder
|
|
133
|
+
private
|
|
134
|
+
|
|
135
|
+
def cmyk_row(c, m, y, k, i, n)
|
|
136
|
+
row = String.new(capacity: n, encoding: Encoding::BINARY)
|
|
137
|
+
stop = i + n
|
|
138
|
+
while i < stop
|
|
139
|
+
kk = 255 - k.getbyte(i)
|
|
140
|
+
r = ((255 - c.getbyte(i)) * kk + 127) / 255
|
|
141
|
+
g = ((255 - m.getbyte(i)) * kk + 127) / 255
|
|
142
|
+
b = ((255 - y.getbyte(i)) * kk + 127) / 255
|
|
143
|
+
row << ((306 * r + 601 * g + 117 * b + 0x200) >> 10)
|
|
144
|
+
i += 1
|
|
145
|
+
end
|
|
146
|
+
row
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# YCCK: libjpeg's YCbCr -> CMY conversion gives R, G, B directly
|
|
150
|
+
# (C = 255 - R, and plain C means R = 255 - C).
|
|
151
|
+
def ycck_row(y, cb, cr, k, i, n)
|
|
152
|
+
row = String.new(capacity: n, encoding: Encoding::BINARY)
|
|
153
|
+
cr_r = QrReaderKernels::JPEG_CR_R
|
|
154
|
+
cb_b = QrReaderKernels::JPEG_CB_B
|
|
155
|
+
cr_g = QrReaderKernels::JPEG_CR_G
|
|
156
|
+
cb_g = QrReaderKernels::JPEG_CB_G
|
|
157
|
+
stop = i + n
|
|
158
|
+
while i < stop
|
|
159
|
+
yy = y.getbyte(i)
|
|
160
|
+
u = cb.getbyte(i)
|
|
161
|
+
v = cr.getbyte(i)
|
|
162
|
+
kk = 255 - k.getbyte(i)
|
|
163
|
+
r = yy + cr_r[v]
|
|
164
|
+
g = yy + ((cb_g[u] + cr_g[v]) >> 16)
|
|
165
|
+
b = yy + cb_b[u]
|
|
166
|
+
r = r < 0 ? 0 : (r > 255 ? 255 : r)
|
|
167
|
+
g = g < 0 ? 0 : (g > 255 ? 255 : g)
|
|
168
|
+
b = b < 0 ? 0 : (b > 255 ? 255 : b)
|
|
169
|
+
r = (r * kk + 127) / 255
|
|
170
|
+
g = (g * kk + 127) / 255
|
|
171
|
+
b = (b * kk + 127) / 255
|
|
172
|
+
row << ((306 * r + 601 * g + 117 * b + 0x200) >> 10)
|
|
173
|
+
i += 1
|
|
174
|
+
end
|
|
175
|
+
row
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
120
179
|
# Entropy-decoded coefficients of a JPEG's luminance components (see
|
|
121
180
|
# JPEG.decode_keeping_coefficients), rendered at any scale by #image.
|
|
122
181
|
class Coefficients
|
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module QrReader
|
|
4
|
+
module Formats
|
|
5
|
+
module JPEG2000
|
|
6
|
+
# One image component as declared by SIZ: sample +precision+ in bits
|
|
7
|
+
# (1..38), +signed+ samples, and the sub-sampling factors +dx+, +dy+.
|
|
8
|
+
Component = Struct.new(:precision, :signed, :dx, :dy)
|
|
9
|
+
|
|
10
|
+
# The component-specific coding style of COD / COC (SPcod / SPcoc):
|
|
11
|
+
# decomposition +levels+, code-block exponents +xcb+, +ycb+ (2..10),
|
|
12
|
+
# code-block style flags, +reversible+ (5/3) or not (9/7), and the
|
|
13
|
+
# precinct exponents [[ppx, ppy], ...] per resolution (0 = lowest).
|
|
14
|
+
CodingStyle = Struct.new(:levels, :xcb, :ycb, :cblk_style, :reversible, :precincts)
|
|
15
|
+
|
|
16
|
+
# QCD / QCC: +style+ (0 none, 1 scalar derived, 2 scalar expounded),
|
|
17
|
+
# +guard_bits+ and +steps+ [[exponent, mantissa], ...] per subband.
|
|
18
|
+
Quantization = Struct.new(:style, :guard_bits, :steps)
|
|
19
|
+
|
|
20
|
+
# One progression of a POC marker (or the single default one).
|
|
21
|
+
Progression = Struct.new(:res_start, :comp_start, :layer_end, :res_end, :comp_end, :order)
|
|
22
|
+
|
|
23
|
+
# Coding parameters that tile-part headers may override: the main
|
|
24
|
+
# header holds the defaults, each tile its own overrides (nil = not
|
|
25
|
+
# set at this level).
|
|
26
|
+
class Params
|
|
27
|
+
attr_accessor :order, :layers, :mct, :sop, :eph, :style, :coc, :qcd, :qcc, :rgn, :pocs, :ppt
|
|
28
|
+
|
|
29
|
+
def initialize
|
|
30
|
+
@coc = {}
|
|
31
|
+
@qcc = {}
|
|
32
|
+
@rgn = {}
|
|
33
|
+
@pocs = nil
|
|
34
|
+
@ppt = nil
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# A tile's tile-parts: header parameters and the byte ranges of the
|
|
39
|
+
# packet data in the codestream.
|
|
40
|
+
class TileInfo
|
|
41
|
+
attr_reader :index, :params, :ranges, :ppm
|
|
42
|
+
attr_accessor :parts
|
|
43
|
+
|
|
44
|
+
def initialize(index)
|
|
45
|
+
@index = index
|
|
46
|
+
@params = Params.new
|
|
47
|
+
@ranges = []
|
|
48
|
+
@ppm = nil
|
|
49
|
+
@parts = 0
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def add_ppm(chunk)
|
|
53
|
+
(@ppm ||= "".b) << chunk
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Parser for a JPEG 2000 codestream (ITU-T T.800 / ISO 15444-1 Annex A):
|
|
58
|
+
# the main header and the index of all tile-parts. Packet data is not
|
|
59
|
+
# touched here.
|
|
60
|
+
class Codestream
|
|
61
|
+
# Part 2 (and later) marker segments that change the decoding.
|
|
62
|
+
PART2_MARKERS = {
|
|
63
|
+
0xFF70 => "DCO", 0xFF71 => "VMS", 0xFF72 => "DFS", 0xFF73 => "ADS", 0xFF74 => "MCT",
|
|
64
|
+
0xFF75 => "MCC", 0xFF76 => "NLT", 0xFF77 => "MCO", 0xFF78 => "CBD", 0xFF79 => "ATK"
|
|
65
|
+
}.freeze
|
|
66
|
+
|
|
67
|
+
# Most tiles a codestream may have (Isot is 16 bits).
|
|
68
|
+
MAX_TILES = 65_535
|
|
69
|
+
|
|
70
|
+
attr_reader :data, :x0, :y0, :x1, :y1, :tile_x0, :tile_y0, :tile_w, :tile_h, :tiles_x, :tiles_y,
|
|
71
|
+
:components, :main, :tiles, :capabilities
|
|
72
|
+
|
|
73
|
+
# Parses the codestream +data+ (a binary String starting with SOC).
|
|
74
|
+
def initialize(data)
|
|
75
|
+
@data = data
|
|
76
|
+
@size = data.bytesize
|
|
77
|
+
raise ImageError, "not a JPEG 2000 codestream" unless @size >= 4 && u16(0) == 0xFF4F
|
|
78
|
+
|
|
79
|
+
raise ImageError, "JPEG 2000 codestream lacks the SIZ marker" unless u16(2) == 0xFF51
|
|
80
|
+
|
|
81
|
+
@main = Params.new
|
|
82
|
+
@tiles = {}
|
|
83
|
+
@ppm_chunks = nil
|
|
84
|
+
pos = parse_main_header(2)
|
|
85
|
+
parse_tile_parts(pos)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# [width, height] of the image in codestream +data+, read from SIZ
|
|
89
|
+
# only.
|
|
90
|
+
def self.image_size(data)
|
|
91
|
+
unless data.bytesize >= 4 + 2 + 38 && data.getbyte(0) == 0xFF && data.getbyte(1) == 0x4F &&
|
|
92
|
+
data.getbyte(2) == 0xFF && data.getbyte(3) == 0x51
|
|
93
|
+
raise ImageError, "not a JPEG 2000 codestream"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
x1, y1, x0, y0 = data.byteslice(8, 16).unpack("N4")
|
|
97
|
+
raise ImageError, "invalid JPEG 2000 image area" unless x1 > x0 && y1 > y0
|
|
98
|
+
|
|
99
|
+
[x1 - x0, y1 - y0]
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Number of tiles in the grid.
|
|
103
|
+
def tile_count
|
|
104
|
+
@tiles_x * @tiles_y
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Reference-grid rectangle [x0, y0, x1, y1] of tile +index+.
|
|
108
|
+
def tile_rect(index)
|
|
109
|
+
p = index % @tiles_x
|
|
110
|
+
q = index / @tiles_x
|
|
111
|
+
[[@tile_x0 + p * @tile_w, @x0].max, [@tile_y0 + q * @tile_h, @y0].max,
|
|
112
|
+
[@tile_x0 + (p + 1) * @tile_w, @x1].min, [@tile_y0 + (q + 1) * @tile_h, @y1].min]
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# The coding parameters of tile +info+ for component +c+:
|
|
116
|
+
# [CodingStyle, Quantization, roi_shift].
|
|
117
|
+
def component_params(info, c)
|
|
118
|
+
t = info.params
|
|
119
|
+
m = @main
|
|
120
|
+
style = t.coc[c] || t.style || m.coc[c] || m.style
|
|
121
|
+
raise ImageError, "JPEG 2000 codestream lacks a COD marker" unless style
|
|
122
|
+
quant = t.qcc[c] || t.qcd || m.qcc[c] || m.qcd
|
|
123
|
+
raise ImageError, "JPEG 2000 codestream lacks a QCD marker" unless quant
|
|
124
|
+
|
|
125
|
+
roi = t.rgn.fetch(c) { m.rgn.fetch(c, 0) }
|
|
126
|
+
[style, quant, roi]
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Tile-wide parameter +name+ (:order, :layers, :mct, :sop, :eph).
|
|
130
|
+
def tile_param(info, name)
|
|
131
|
+
value = info.params.public_send(name)
|
|
132
|
+
value.nil? ? @main.public_send(name) : value
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# The progressions of tile +info+ (its POC, else the main POC, else
|
|
136
|
+
# the default progression order over everything).
|
|
137
|
+
def progressions(info)
|
|
138
|
+
pocs = info.params.pocs || @main.pocs
|
|
139
|
+
return pocs if pocs
|
|
140
|
+
|
|
141
|
+
[Progression.new(0, 0, tile_param(info, :layers), 33, @components.size, tile_param(info, :order))]
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
private
|
|
145
|
+
|
|
146
|
+
def u16(pos)
|
|
147
|
+
(@data.getbyte(pos) << 8) | @data.getbyte(pos + 1)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def u32(pos)
|
|
151
|
+
(u16(pos) << 16) | u16(pos + 2)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Reads the marker segment at +pos+: [marker, body start, body end].
|
|
155
|
+
def segment(pos)
|
|
156
|
+
raise ImageError, "truncated JPEG 2000 header" if pos + 4 > @size
|
|
157
|
+
|
|
158
|
+
marker = u16(pos)
|
|
159
|
+
raise ImageError, format("invalid JPEG 2000 marker 0x%04X", marker) if marker < 0xFF00
|
|
160
|
+
|
|
161
|
+
length = u16(pos + 2)
|
|
162
|
+
raise ImageError, format("invalid length of JPEG 2000 marker 0x%04X", marker) if length < 2
|
|
163
|
+
raise ImageError, "truncated JPEG 2000 header" if pos + 2 + length > @size
|
|
164
|
+
|
|
165
|
+
[marker, pos + 4, pos + 2 + length]
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def parse_main_header(pos)
|
|
169
|
+
_, body, stop = segment(pos)
|
|
170
|
+
parse_siz(body, stop)
|
|
171
|
+
pos = stop
|
|
172
|
+
loop do
|
|
173
|
+
raise ImageError, "truncated JPEG 2000 main header" if pos + 2 > @size
|
|
174
|
+
|
|
175
|
+
marker = u16(pos)
|
|
176
|
+
return pos if marker == 0xFF90
|
|
177
|
+
raise ImageError, "JPEG 2000 codestream contains no tiles" if marker == 0xFFD9
|
|
178
|
+
|
|
179
|
+
marker, body, stop = segment(pos)
|
|
180
|
+
parse_header_marker(marker, body, stop, @main, true)
|
|
181
|
+
pos = stop
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def parse_siz(pos, stop)
|
|
186
|
+
raise ImageError, "invalid JPEG 2000 SIZ marker" if stop - pos < 38
|
|
187
|
+
|
|
188
|
+
@capabilities = u16(pos)
|
|
189
|
+
if @capabilities & 0x8000 != 0
|
|
190
|
+
raise UnsupportedImageError, "JPEG 2000 Part 2 (JPX extension) codestreams are not supported"
|
|
191
|
+
end
|
|
192
|
+
if @capabilities & 0x4000 != 0
|
|
193
|
+
raise UnsupportedImageError, "High-throughput JPEG 2000 (HTJ2K) codestreams are not supported"
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
@x1, @y1, @x0, @y0, @tile_w, @tile_h, @tile_x0, @tile_y0 = (0...8).map { |i| u32(pos + 2 + 4 * i) }
|
|
197
|
+
count = u16(pos + 34)
|
|
198
|
+
raise ImageError, "invalid JPEG 2000 component count #{count}" if count < 1 || stop - pos < 36 + 3 * count
|
|
199
|
+
raise ImageError, "invalid JPEG 2000 image area" unless @x1 > @x0 && @y1 > @y0
|
|
200
|
+
unless @tile_w.positive? && @tile_h.positive? && @tile_x0 <= @x0 && @tile_y0 <= @y0 &&
|
|
201
|
+
@tile_x0 + @tile_w > @x0 && @tile_y0 + @tile_h > @y0
|
|
202
|
+
raise ImageError, "invalid JPEG 2000 tile grid"
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
@tiles_x = (@x1 - @tile_x0 + @tile_w - 1) / @tile_w
|
|
206
|
+
@tiles_y = (@y1 - @tile_y0 + @tile_h - 1) / @tile_h
|
|
207
|
+
raise ImageError, "too many JPEG 2000 tiles (#{@tiles_x}x#{@tiles_y})" if @tiles_x * @tiles_y > MAX_TILES
|
|
208
|
+
|
|
209
|
+
@components = Array.new(count) do |i|
|
|
210
|
+
ssiz = @data.getbyte(pos + 36 + 3 * i)
|
|
211
|
+
dx = @data.getbyte(pos + 37 + 3 * i)
|
|
212
|
+
dy = @data.getbyte(pos + 38 + 3 * i)
|
|
213
|
+
raise ImageError, "invalid JPEG 2000 component sub-sampling" if dx.zero? || dy.zero?
|
|
214
|
+
|
|
215
|
+
precision = (ssiz & 0x7F) + 1
|
|
216
|
+
raise UnsupportedImageError, "JPEG 2000 samples of #{precision} bits are not supported" if precision > 30
|
|
217
|
+
|
|
218
|
+
Component.new(precision, ssiz >= 0x80, dx, dy)
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Parses a main or tile-part header marker segment into +params+.
|
|
223
|
+
def parse_header_marker(marker, pos, stop, params, main)
|
|
224
|
+
case marker
|
|
225
|
+
when 0xFF52 then parse_cod(pos, stop, params)
|
|
226
|
+
when 0xFF53 then parse_coc(pos, stop, params)
|
|
227
|
+
when 0xFF5C then params.qcd = parse_quant(pos, stop)
|
|
228
|
+
when 0xFF5D then parse_qcc(pos, stop, params)
|
|
229
|
+
when 0xFF5E then parse_rgn(pos, stop, params)
|
|
230
|
+
when 0xFF5F then parse_poc(pos, stop, params)
|
|
231
|
+
when 0xFF60
|
|
232
|
+
raise ImageError, "PPM marker in a JPEG 2000 tile-part header" unless main
|
|
233
|
+
|
|
234
|
+
(@ppm_chunks ||= []) << [@data.getbyte(pos), @data.byteslice(pos + 1, stop - pos - 1)]
|
|
235
|
+
when 0xFF61
|
|
236
|
+
raise ImageError, "PPT marker in the JPEG 2000 main header" if main
|
|
237
|
+
|
|
238
|
+
(params.ppt ||= []) << [@data.getbyte(pos), @data.byteslice(pos + 1, stop - pos - 1)]
|
|
239
|
+
when 0xFF51 then raise ImageError, "duplicate JPEG 2000 SIZ marker"
|
|
240
|
+
else
|
|
241
|
+
name = PART2_MARKERS[marker]
|
|
242
|
+
raise UnsupportedImageError, "JPEG 2000 Part 2 marker #{name} is not supported" if name
|
|
243
|
+
# TLM, PLM, PLT, CRG, COM, CAP, CPF and unknown segments: skipped.
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def component_index(pos)
|
|
248
|
+
if @components.size < 257
|
|
249
|
+
[@data.getbyte(pos), pos + 1]
|
|
250
|
+
else
|
|
251
|
+
[u16(pos), pos + 2]
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def check_component(c)
|
|
256
|
+
raise ImageError, "JPEG 2000 marker for nonexistent component #{c}" if c >= @components.size
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def parse_cod(pos, stop, params)
|
|
260
|
+
raise ImageError, "invalid JPEG 2000 COD marker" if stop - pos < 10
|
|
261
|
+
|
|
262
|
+
scod = @data.getbyte(pos)
|
|
263
|
+
order = @data.getbyte(pos + 1)
|
|
264
|
+
raise ImageError, "invalid JPEG 2000 progression order #{order}" if order > 4
|
|
265
|
+
|
|
266
|
+
layers = u16(pos + 2)
|
|
267
|
+
raise ImageError, "invalid JPEG 2000 layer count" if layers.zero?
|
|
268
|
+
|
|
269
|
+
mct = @data.getbyte(pos + 4)
|
|
270
|
+
raise UnsupportedImageError, "JPEG 2000 Part 2 multi-component transforms are not supported" if mct > 1
|
|
271
|
+
|
|
272
|
+
params.order = order
|
|
273
|
+
params.layers = layers
|
|
274
|
+
params.mct = mct
|
|
275
|
+
params.sop = scod & 2 != 0
|
|
276
|
+
params.eph = scod & 4 != 0
|
|
277
|
+
params.style = parse_spcod(pos + 5, stop, scod & 1 != 0)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def parse_coc(pos, stop, params)
|
|
281
|
+
c, pos = component_index(pos)
|
|
282
|
+
check_component(c)
|
|
283
|
+
raise ImageError, "invalid JPEG 2000 COC marker" if pos >= stop
|
|
284
|
+
|
|
285
|
+
params.coc[c] = parse_spcod(pos + 1, stop, @data.getbyte(pos) & 1 != 0)
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def parse_spcod(pos, stop, precincts)
|
|
289
|
+
raise ImageError, "invalid JPEG 2000 coding style marker" if stop - pos < 5
|
|
290
|
+
|
|
291
|
+
levels = @data.getbyte(pos)
|
|
292
|
+
raise ImageError, "invalid JPEG 2000 decomposition level count #{levels}" if levels > 32
|
|
293
|
+
|
|
294
|
+
xcb = @data.getbyte(pos + 1) + 2
|
|
295
|
+
ycb = @data.getbyte(pos + 2) + 2
|
|
296
|
+
raise ImageError, "invalid JPEG 2000 code-block size" if xcb > 10 || ycb > 10 || xcb + ycb > 12
|
|
297
|
+
|
|
298
|
+
style = @data.getbyte(pos + 3)
|
|
299
|
+
raise UnsupportedImageError, "high-throughput (HTJ2K) code-blocks are not supported" if style & 0xC0 != 0
|
|
300
|
+
|
|
301
|
+
transform = @data.getbyte(pos + 4)
|
|
302
|
+
raise UnsupportedImageError, "JPEG 2000 Part 2 wavelet kernels are not supported" if transform > 1
|
|
303
|
+
|
|
304
|
+
sizes = Array.new(levels + 1) do |r|
|
|
305
|
+
next [15, 15] unless precincts
|
|
306
|
+
|
|
307
|
+
raise ImageError, "truncated JPEG 2000 precinct sizes" if pos + 5 + r >= stop
|
|
308
|
+
|
|
309
|
+
b = @data.getbyte(pos + 5 + r)
|
|
310
|
+
ppx = b & 15
|
|
311
|
+
ppy = b >> 4
|
|
312
|
+
raise ImageError, "invalid JPEG 2000 precinct size" if r.positive? && (ppx.zero? || ppy.zero?)
|
|
313
|
+
|
|
314
|
+
[ppx, ppy]
|
|
315
|
+
end
|
|
316
|
+
CodingStyle.new(levels, xcb, ycb, style, transform == 1, sizes)
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def parse_quant(pos, stop)
|
|
320
|
+
raise ImageError, "invalid JPEG 2000 quantization marker" if pos >= stop
|
|
321
|
+
|
|
322
|
+
sq = @data.getbyte(pos)
|
|
323
|
+
style = sq & 0x1F
|
|
324
|
+
guard = sq >> 5
|
|
325
|
+
pos += 1
|
|
326
|
+
steps = []
|
|
327
|
+
case style
|
|
328
|
+
when 0
|
|
329
|
+
while pos < stop
|
|
330
|
+
steps << [@data.getbyte(pos) >> 3, 0]
|
|
331
|
+
pos += 1
|
|
332
|
+
end
|
|
333
|
+
when 1, 2
|
|
334
|
+
while pos + 1 < stop
|
|
335
|
+
v = u16(pos)
|
|
336
|
+
steps << [v >> 11, v & 0x7FF]
|
|
337
|
+
pos += 2
|
|
338
|
+
break if style == 1
|
|
339
|
+
end
|
|
340
|
+
else
|
|
341
|
+
raise ImageError, "invalid JPEG 2000 quantization style #{style}"
|
|
342
|
+
end
|
|
343
|
+
raise ImageError, "JPEG 2000 quantization marker without step sizes" if steps.empty?
|
|
344
|
+
|
|
345
|
+
Quantization.new(style, guard, steps)
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def parse_qcc(pos, stop, params)
|
|
349
|
+
c, pos = component_index(pos)
|
|
350
|
+
check_component(c)
|
|
351
|
+
params.qcc[c] = parse_quant(pos, stop)
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def parse_rgn(pos, stop, params)
|
|
355
|
+
c, pos = component_index(pos)
|
|
356
|
+
check_component(c)
|
|
357
|
+
raise ImageError, "invalid JPEG 2000 RGN marker" if pos + 2 > stop
|
|
358
|
+
raise UnsupportedImageError, "JPEG 2000 ROI style #{@data.getbyte(pos)}" unless @data.getbyte(pos).zero?
|
|
359
|
+
|
|
360
|
+
params.rgn[c] = @data.getbyte(pos + 1)
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def parse_poc(pos, stop, params)
|
|
364
|
+
wide = @components.size >= 257
|
|
365
|
+
entry = wide ? 9 : 7
|
|
366
|
+
list = params.pocs || []
|
|
367
|
+
while pos + entry <= stop
|
|
368
|
+
rs = @data.getbyte(pos)
|
|
369
|
+
cs, p = component_index(pos + 1)
|
|
370
|
+
le = u16(p)
|
|
371
|
+
re = @data.getbyte(p + 2)
|
|
372
|
+
ce, p = component_index(p + 3)
|
|
373
|
+
ce = 256 if ce.zero? && !wide
|
|
374
|
+
order = @data.getbyte(p)
|
|
375
|
+
raise ImageError, "invalid JPEG 2000 POC progression order" if order > 4
|
|
376
|
+
|
|
377
|
+
list << Progression.new(rs, cs, le, re, ce, order)
|
|
378
|
+
pos += entry
|
|
379
|
+
end
|
|
380
|
+
params.pocs = list
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def parse_tile_parts(pos)
|
|
384
|
+
ppm = ppm_stream
|
|
385
|
+
ppm_pos = 0
|
|
386
|
+
while pos + 12 <= @size && u16(pos) == 0xFF90
|
|
387
|
+
raise ImageError, "invalid JPEG 2000 SOT marker" unless u16(pos + 2) == 10
|
|
388
|
+
|
|
389
|
+
index = u16(pos + 4)
|
|
390
|
+
psot = u32(pos + 6)
|
|
391
|
+
raise ImageError, "invalid JPEG 2000 tile index #{index}" if index >= tile_count
|
|
392
|
+
|
|
393
|
+
last = psot.zero?
|
|
394
|
+
stop = last ? @size : pos + psot
|
|
395
|
+
raise ImageError, "invalid JPEG 2000 tile-part length" if !last && psot < 14
|
|
396
|
+
|
|
397
|
+
stop = @size if stop > @size
|
|
398
|
+
info = (@tiles[index] ||= TileInfo.new(index))
|
|
399
|
+
info.parts += 1
|
|
400
|
+
hpos = pos + 12
|
|
401
|
+
loop do
|
|
402
|
+
break if hpos + 2 > stop
|
|
403
|
+
|
|
404
|
+
marker = u16(hpos)
|
|
405
|
+
if marker == 0xFF93
|
|
406
|
+
hpos += 2
|
|
407
|
+
break
|
|
408
|
+
end
|
|
409
|
+
marker, body, seg_end = segment(hpos)
|
|
410
|
+
parse_header_marker(marker, body, seg_end, info.params, false)
|
|
411
|
+
hpos = seg_end
|
|
412
|
+
end
|
|
413
|
+
info.ranges << [hpos, stop] if hpos < stop
|
|
414
|
+
if ppm
|
|
415
|
+
raise ImageError, "JPEG 2000 PPM marker too short" if ppm_pos + 4 > ppm.bytesize
|
|
416
|
+
|
|
417
|
+
n = (ppm.getbyte(ppm_pos) << 24) | (ppm.getbyte(ppm_pos + 1) << 16) |
|
|
418
|
+
(ppm.getbyte(ppm_pos + 2) << 8) | ppm.getbyte(ppm_pos + 3)
|
|
419
|
+
info.add_ppm(ppm.byteslice(ppm_pos + 4, n) || "".b)
|
|
420
|
+
ppm_pos += 4 + n
|
|
421
|
+
end
|
|
422
|
+
pos = stop
|
|
423
|
+
break if last
|
|
424
|
+
end
|
|
425
|
+
finish_ppt
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
# All PPM data of the main header in Zppm order, or nil.
|
|
429
|
+
def ppm_stream
|
|
430
|
+
return nil unless @ppm_chunks
|
|
431
|
+
|
|
432
|
+
@ppm_chunks.sort_by.with_index { |(z, _), i| [z, i] }.map(&:last).join.b
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
# Joins each tile's PPT chunks in Zppt order.
|
|
436
|
+
def finish_ppt
|
|
437
|
+
@tiles.each_value do |info|
|
|
438
|
+
chunks = info.params.ppt
|
|
439
|
+
next unless chunks
|
|
440
|
+
|
|
441
|
+
info.params.ppt = chunks.sort_by.with_index { |(z, _), i| [z, i] }.map(&:last).join.b
|
|
442
|
+
end
|
|
443
|
+
end
|
|
444
|
+
end
|
|
445
|
+
end
|
|
446
|
+
end
|
|
447
|
+
end
|