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.
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QrReader
4
+ module Formats
5
+ module JPEG2000
6
+ # A JP2 palette (pclr box): +entries+ is an Array of rows, one Integer
7
+ # per column; +depths+ / +signed+ describe each column.
8
+ Palette = Struct.new(:entries, :depths, :signed)
9
+
10
+ # Channel definition (cdef): channel index, type (0 colour, 1
11
+ # opacity, 2 premultiplied opacity, 65535 unspecified) and the colour
12
+ # it belongs to (0 whole image, 1..n colour n, 65535 none).
13
+ ChannelDef = Struct.new(:channel, :type, :assoc)
14
+
15
+ # What the JP2 boxes say about a codestream.
16
+ Header = Struct.new(:codestream, :enum_cs, :icc_space, :palette, :cmap, :cdef)
17
+
18
+ # JP2 / JPX file format boxes (ITU-T T.800 Annex I): finds the
19
+ # codestream and the colour information. Raw codestreams (starting
20
+ # with SOC SIZ) are accepted too.
21
+ module Container
22
+ SIGNATURE = "\x00\x00\x00\x0CjP \r\n\x87\n".b.freeze
23
+ SOC_SIZ = "\xFF\x4F\xFF\x51".b.freeze
24
+
25
+ module_function
26
+
27
+ # Parses +bytes+ (a binary String) into a Header.
28
+ def parse(bytes)
29
+ return Header.new(bytes) if bytes.start_with?(SOC_SIZ)
30
+ raise ImageError, "not a JPEG 2000 file" unless bytes.start_with?(SIGNATURE)
31
+
32
+ header = Header.new
33
+ each_box(bytes, 0, bytes.bytesize, true) do |type, start, stop|
34
+ case type
35
+ when "jp2h" then parse_jp2h(bytes, start, stop, header)
36
+ when "jp2c"
37
+ header.codestream = bytes.byteslice(start, stop - start)
38
+ break
39
+ end
40
+ end
41
+ raise ImageError, "JPEG 2000 file contains no codestream" unless header.codestream
42
+
43
+ header
44
+ end
45
+
46
+ # Yields [type, content start, content end] of the boxes in
47
+ # bytes[pos...stop]. A box running past the end is clipped (a
48
+ # truncated file) when +lenient+.
49
+ def each_box(bytes, pos, stop, lenient)
50
+ while pos + 8 <= stop
51
+ length = bytes.byteslice(pos, 4).unpack1("N")
52
+ type = bytes.byteslice(pos + 4, 4)
53
+ header = 8
54
+ if length == 1
55
+ raise ImageError, "truncated JPEG 2000 box" if pos + 16 > stop
56
+
57
+ length = bytes.byteslice(pos + 8, 8).unpack1("Q>")
58
+ header = 16
59
+ elsif length.zero?
60
+ length = stop - pos
61
+ end
62
+ raise ImageError, "invalid JPEG 2000 box length" if length < header
63
+
64
+ box_end = pos + length
65
+ if box_end > stop
66
+ raise ImageError, "truncated JPEG 2000 box" unless lenient
67
+
68
+ box_end = stop
69
+ end
70
+ yield type, pos + header, box_end
71
+ pos = box_end
72
+ end
73
+ end
74
+
75
+ def parse_jp2h(bytes, pos, stop, header)
76
+ each_box(bytes, pos, stop, true) do |type, start, box_end|
77
+ case type
78
+ when "colr" then parse_colr(bytes, start, box_end, header)
79
+ when "pclr" then header.palette = parse_pclr(bytes, start, box_end)
80
+ when "cmap" then header.cmap = parse_cmap(bytes, start, box_end)
81
+ when "cdef" then header.cdef = parse_cdef(bytes, start, box_end)
82
+ end
83
+ end
84
+ end
85
+
86
+ # Only the first colr box counts (I.5.3.3).
87
+ def parse_colr(bytes, pos, stop, header)
88
+ return if header.enum_cs || header.icc_space || stop - pos < 3
89
+
90
+ method = bytes.getbyte(pos)
91
+ if method == 1
92
+ header.enum_cs = bytes.byteslice(pos + 3, 4).unpack1("N") if stop - pos >= 7
93
+ elsif method == 2 || method == 3
94
+ # The ICC profile header's colour space signature (offset 16).
95
+ header.icc_space = bytes.byteslice(pos + 3 + 16, 4) if stop - pos >= 3 + 20
96
+ end
97
+ end
98
+
99
+ def parse_pclr(bytes, pos, stop)
100
+ raise ImageError, "invalid JPEG 2000 palette" if stop - pos < 3
101
+
102
+ count = bytes.byteslice(pos, 2).unpack1("n")
103
+ columns = bytes.getbyte(pos + 2)
104
+ raise ImageError, "invalid JPEG 2000 palette" if count.zero? || count > 1024 || columns.zero?
105
+
106
+ pos += 3
107
+ raise ImageError, "truncated JPEG 2000 palette" if pos + columns > stop
108
+
109
+ depths = Array.new(columns) { |i| (bytes.getbyte(pos + i) & 0x7F) + 1 }
110
+ signed = Array.new(columns) { |i| bytes.getbyte(pos + i) >= 0x80 }
111
+ raise UnsupportedImageError, "JPEG 2000 palette entries over 32 bits" if depths.max > 32
112
+
113
+ pos += columns
114
+ sizes = depths.map { |d| (d + 7) >> 3 }
115
+ raise ImageError, "truncated JPEG 2000 palette" if pos + count * sizes.sum > stop
116
+
117
+ entries = Array.new(count) do
118
+ sizes.map do |size|
119
+ v = 0
120
+ size.times do
121
+ v = (v << 8) | bytes.getbyte(pos)
122
+ pos += 1
123
+ end
124
+ v
125
+ end
126
+ end
127
+ Palette.new(entries, depths, signed)
128
+ end
129
+
130
+ def parse_cmap(bytes, pos, stop)
131
+ list = []
132
+ while pos + 4 <= stop
133
+ list << [bytes.byteslice(pos, 2).unpack1("n"), bytes.getbyte(pos + 2), bytes.getbyte(pos + 3)]
134
+ pos += 4
135
+ end
136
+ list
137
+ end
138
+
139
+ def parse_cdef(bytes, pos, stop)
140
+ return nil if stop - pos < 2
141
+
142
+ n = bytes.byteslice(pos, 2).unpack1("n")
143
+ pos += 2
144
+ list = []
145
+ n.times do
146
+ break if pos + 6 > stop
147
+
148
+ list << ChannelDef.new(*bytes.byteslice(pos, 6).unpack("n3"))
149
+ pos += 6
150
+ end
151
+ list
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end