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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8eabc5559f8483ccce0478d0910040e632a16c36f742eff00c8db2af43b69fad
4
- data.tar.gz: b6c9afad43a6996e18791869703c23aa5730719dbede44e96099fbbc84967f80
3
+ metadata.gz: 676cc0ecf84f68ef07fc1b05b6c473c301eb4b82ee8ab615dc30bb036030834f
4
+ data.tar.gz: c7b893b1828be74bb1f4feed942b325b34b8183dc7c318919fb891f84b64b24d
5
5
  SHA512:
6
- metadata.gz: f9f50d8bb7cfffa3ddd89b11898e458e4f346975816df4d7362470c4c1afa43fb79df92c751c4ae76a00e7cd150b8eef3036e5342e025f00f6f20b1c6ccf5717
7
- data.tar.gz: 5c71745f5e75ac79d759f6f22364020a72e99c3b3350e1a1bf06849f14b344b0ac0a9143f73e0fc96dbd6b868406097f19576485a8a25dfe433a6c0cc9ab659d
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 G3/G4 fax
6
- compression) and the images inside scanned PDFs.
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
- - Scanned PDFs: images compressed with JPEG, CCITT fax, JBIG2 (generic
142
- regions), Flate, LZW, run-length; xref and object streams; PDFs encrypted
143
- with an empty user password.
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
- Vector-drawn QR codes in digitally created PDFs are not rendered — only raster
147
- images embedded in the PDF are scanned. JPEG 2000 images in PDFs are skipped.
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 PNM images (use - for stdin)."
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
- def decode(bytes, scale: 1)
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 = guard("data") { QrReaderKernels.jpeg_luma(bytes, scale) }
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