pure_jpeg 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +42 -0
- data/LICENSE +1 -1
- data/README.md +73 -16
- data/lib/pure_jpeg/bit_reader.rb +8 -1
- data/lib/pure_jpeg/bit_writer.rb +4 -4
- data/lib/pure_jpeg/decoder.rb +337 -15
- data/lib/pure_jpeg/encoder.rb +217 -68
- data/lib/pure_jpeg/huffman/decoder.rb +1 -1
- data/lib/pure_jpeg/huffman/encoder.rb +73 -45
- data/lib/pure_jpeg/huffman/tables.rb +93 -1
- data/lib/pure_jpeg/image.rb +40 -8
- data/lib/pure_jpeg/info.rb +6 -0
- data/lib/pure_jpeg/jfif_reader.rb +74 -21
- data/lib/pure_jpeg/source/chunky_png_source.rb +8 -5
- data/lib/pure_jpeg/source/raw_source.rb +2 -1
- data/lib/pure_jpeg/version.rb +1 -1
- data/lib/pure_jpeg.rb +30 -0
- metadata +3 -2
data/lib/pure_jpeg/image.rb
CHANGED
|
@@ -3,22 +3,33 @@
|
|
|
3
3
|
module PureJPEG
|
|
4
4
|
# A decoded JPEG image with pixel-level access.
|
|
5
5
|
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
6
|
+
# Internally stores pixels as packed integers (+r << 16 | g << 8 | b+) to
|
|
7
|
+
# avoid per-pixel object allocation. Implements the same pixel source
|
|
8
|
+
# interface (+width+, +height+, +[x, y]+) as encoder inputs, so a decoded
|
|
9
|
+
# image can be passed directly to {PureJPEG.encode} for re-encoding.
|
|
9
10
|
class Image
|
|
10
11
|
# @return [Integer] image width in pixels
|
|
11
12
|
attr_reader :width
|
|
12
13
|
# @return [Integer] image height in pixels
|
|
13
14
|
attr_reader :height
|
|
14
15
|
|
|
16
|
+
# @return [Array<Integer>] flat row-major array of packed RGB integers.
|
|
17
|
+
# Format: +(r << 16) | (g << 8) | b+.
|
|
18
|
+
attr_reader :packed_pixels
|
|
19
|
+
|
|
20
|
+
# @return [String, nil] raw ICC color profile data, if present in the source JPEG
|
|
21
|
+
attr_reader :icc_profile
|
|
22
|
+
|
|
15
23
|
# @param width [Integer]
|
|
16
24
|
# @param height [Integer]
|
|
17
|
-
# @param
|
|
18
|
-
|
|
25
|
+
# @param packed_pixels [Array<Integer>] flat row-major array of packed RGB
|
|
26
|
+
# integers in the format +(r << 16) | (g << 8) | b+
|
|
27
|
+
# @param icc_profile [String, nil] raw ICC profile bytes
|
|
28
|
+
def initialize(width, height, packed_pixels, icc_profile: nil)
|
|
19
29
|
@width = width
|
|
20
30
|
@height = height
|
|
21
|
-
@
|
|
31
|
+
@packed_pixels = packed_pixels
|
|
32
|
+
@icc_profile = icc_profile
|
|
22
33
|
end
|
|
23
34
|
|
|
24
35
|
# Retrieve a pixel by coordinate.
|
|
@@ -27,7 +38,8 @@ module PureJPEG
|
|
|
27
38
|
# @param y [Integer] row (0-based)
|
|
28
39
|
# @return [Source::Pixel] pixel with +.r+, +.g+, +.b+ in 0-255
|
|
29
40
|
def [](x, y)
|
|
30
|
-
@
|
|
41
|
+
color = @packed_pixels[y * @width + x]
|
|
42
|
+
Source::Pixel.new((color >> 16) & 0xFF, (color >> 8) & 0xFF, color & 0xFF)
|
|
31
43
|
end
|
|
32
44
|
|
|
33
45
|
# Set a pixel by coordinate.
|
|
@@ -37,7 +49,8 @@ module PureJPEG
|
|
|
37
49
|
# @param pixel [Source::Pixel] replacement pixel
|
|
38
50
|
# @return [Source::Pixel]
|
|
39
51
|
def []=(x, y, pixel)
|
|
40
|
-
@
|
|
52
|
+
@packed_pixels[y * @width + x] = (pixel.r << 16) | (pixel.g << 8) | pixel.b
|
|
53
|
+
pixel
|
|
41
54
|
end
|
|
42
55
|
|
|
43
56
|
# Iterate over every pixel in the image.
|
|
@@ -53,5 +66,24 @@ module PureJPEG
|
|
|
53
66
|
end
|
|
54
67
|
end
|
|
55
68
|
end
|
|
69
|
+
|
|
70
|
+
# Iterate over every pixel without allocating Pixel structs.
|
|
71
|
+
#
|
|
72
|
+
# @yieldparam x [Integer] column
|
|
73
|
+
# @yieldparam y [Integer] row
|
|
74
|
+
# @yieldparam r [Integer] red component (0-255)
|
|
75
|
+
# @yieldparam g [Integer] green component (0-255)
|
|
76
|
+
# @yieldparam b [Integer] blue component (0-255)
|
|
77
|
+
# @return [void]
|
|
78
|
+
def each_rgb
|
|
79
|
+
i = 0
|
|
80
|
+
@height.times do |y|
|
|
81
|
+
@width.times do |x|
|
|
82
|
+
color = @packed_pixels[i]
|
|
83
|
+
yield x, y, (color >> 16) & 0xFF, (color >> 8) & 0xFF, color & 0xFF
|
|
84
|
+
i += 1
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
56
88
|
end
|
|
57
89
|
end
|
|
@@ -3,20 +3,33 @@
|
|
|
3
3
|
module PureJPEG
|
|
4
4
|
class JFIFReader
|
|
5
5
|
attr_reader :width, :height, :components, :quant_tables, :huffman_tables,
|
|
6
|
-
:
|
|
6
|
+
:restart_interval, :progressive, :scans, :icc_profile
|
|
7
7
|
|
|
8
8
|
Component = Struct.new(:id, :h_sampling, :v_sampling, :qt_id)
|
|
9
9
|
ScanComponent = Struct.new(:id, :dc_table_id, :ac_table_id)
|
|
10
|
+
Scan = Struct.new(:components, :spectral_start, :spectral_end, :successive_high, :successive_low, :data, :huffman_tables)
|
|
10
11
|
|
|
11
|
-
def initialize(data)
|
|
12
|
+
def initialize(data, stop_after_frame: false)
|
|
12
13
|
@data = data.b
|
|
14
|
+
@stop_after_frame = stop_after_frame
|
|
13
15
|
@pos = 0
|
|
14
16
|
@quant_tables = {}
|
|
15
17
|
@huffman_tables = {}
|
|
16
18
|
@components = []
|
|
17
|
-
@scan_components = []
|
|
18
19
|
@restart_interval = 0
|
|
20
|
+
@progressive = false
|
|
21
|
+
@scans = []
|
|
22
|
+
@icc_chunks = {}
|
|
19
23
|
parse
|
|
24
|
+
assemble_icc_profile
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def scan_components
|
|
28
|
+
@scans.first&.components || []
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def scan_data
|
|
32
|
+
@scans.first&.data || "".b
|
|
20
33
|
end
|
|
21
34
|
|
|
22
35
|
private
|
|
@@ -27,7 +40,9 @@ module PureJPEG
|
|
|
27
40
|
loop do
|
|
28
41
|
marker = read_marker
|
|
29
42
|
case marker
|
|
30
|
-
when
|
|
43
|
+
when 0xE2 # APP2 (may contain ICC profile)
|
|
44
|
+
parse_app2
|
|
45
|
+
when 0xE0, 0xE1, 0xE3..0xEF # APP0, APP1, APP3-APP15
|
|
31
46
|
skip_segment
|
|
32
47
|
when 0xDB # DQT
|
|
33
48
|
parse_dqt
|
|
@@ -35,14 +50,23 @@ module PureJPEG
|
|
|
35
50
|
parse_dht
|
|
36
51
|
when 0xC0 # SOF0 (baseline)
|
|
37
52
|
parse_sof0
|
|
53
|
+
return if @stop_after_frame
|
|
54
|
+
when 0xC2 # SOF2 (progressive)
|
|
55
|
+
parse_sof0
|
|
56
|
+
@progressive = true
|
|
57
|
+
return if @stop_after_frame
|
|
38
58
|
when 0xDA # SOS
|
|
39
|
-
parse_sos
|
|
40
|
-
extract_scan_data
|
|
41
|
-
|
|
59
|
+
scan = parse_sos
|
|
60
|
+
scan.data = extract_scan_data
|
|
61
|
+
scan.huffman_tables = @huffman_tables.dup
|
|
62
|
+
@scans << scan
|
|
63
|
+
return unless @progressive
|
|
42
64
|
when 0xFE # COM (comment)
|
|
43
65
|
skip_segment
|
|
44
66
|
when 0xDD # DRI (restart interval)
|
|
45
67
|
parse_dri
|
|
68
|
+
when 0xD9 # EOI
|
|
69
|
+
return
|
|
46
70
|
else
|
|
47
71
|
skip_segment
|
|
48
72
|
end
|
|
@@ -50,6 +74,7 @@ module PureJPEG
|
|
|
50
74
|
end
|
|
51
75
|
|
|
52
76
|
def read_byte
|
|
77
|
+
raise PureJPEG::DecodeError, "Unexpected end of JPEG data" if @pos >= @data.bytesize
|
|
53
78
|
byte = @data.getbyte(@pos)
|
|
54
79
|
@pos += 1
|
|
55
80
|
byte
|
|
@@ -61,7 +86,7 @@ module PureJPEG
|
|
|
61
86
|
|
|
62
87
|
def read_marker
|
|
63
88
|
byte = read_byte
|
|
64
|
-
raise "Expected 0xFF, got 0x#{byte.to_s(16)}" unless byte == 0xFF
|
|
89
|
+
raise PureJPEG::DecodeError, "Expected 0xFF, got 0x#{byte.to_s(16)}" unless byte == 0xFF
|
|
65
90
|
# Skip padding 0xFF bytes
|
|
66
91
|
code = read_byte
|
|
67
92
|
code = read_byte while code == 0xFF
|
|
@@ -70,7 +95,29 @@ module PureJPEG
|
|
|
70
95
|
|
|
71
96
|
def expect_marker(expected)
|
|
72
97
|
marker = read_marker
|
|
73
|
-
raise "Expected marker 0x#{expected.to_s(16)}, got 0x#{marker.to_s(16)}" unless marker == expected
|
|
98
|
+
raise PureJPEG::DecodeError, "Expected marker 0x#{expected.to_s(16)}, got 0x#{marker.to_s(16)}" unless marker == expected
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
ICC_PROFILE_SIG = "ICC_PROFILE\0".b
|
|
102
|
+
|
|
103
|
+
def parse_app2
|
|
104
|
+
length = read_u16
|
|
105
|
+
end_pos = @pos + length - 2
|
|
106
|
+
|
|
107
|
+
if length >= 16 && @data[@pos, 12] == ICC_PROFILE_SIG
|
|
108
|
+
@pos += 12
|
|
109
|
+
seq_no = read_byte
|
|
110
|
+
_total = read_byte
|
|
111
|
+
@icc_chunks[seq_no] = @data[@pos, end_pos - @pos]
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
@pos = end_pos
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def assemble_icc_profile
|
|
118
|
+
return if @icc_chunks.empty?
|
|
119
|
+
|
|
120
|
+
@icc_profile = @icc_chunks.sort_by(&:first).map(&:last).join.b
|
|
74
121
|
end
|
|
75
122
|
|
|
76
123
|
def skip_segment
|
|
@@ -136,7 +183,7 @@ module PureJPEG
|
|
|
136
183
|
read_u16 # length
|
|
137
184
|
num_components = read_byte
|
|
138
185
|
|
|
139
|
-
|
|
186
|
+
components = Array.new(num_components) do
|
|
140
187
|
id = read_byte
|
|
141
188
|
tables = read_byte
|
|
142
189
|
dc_id = (tables >> 4) & 0x0F
|
|
@@ -144,8 +191,12 @@ module PureJPEG
|
|
|
144
191
|
ScanComponent.new(id, dc_id, ac_id)
|
|
145
192
|
end
|
|
146
193
|
|
|
147
|
-
#
|
|
148
|
-
|
|
194
|
+
ss = read_byte # spectral selection start
|
|
195
|
+
se = read_byte # spectral selection end
|
|
196
|
+
ahl = read_byte # successive approximation
|
|
197
|
+
ah = (ahl >> 4) & 0x0F
|
|
198
|
+
al = ahl & 0x0F
|
|
199
|
+
Scan.new(components, ss, se, ah, al, nil)
|
|
149
200
|
end
|
|
150
201
|
|
|
151
202
|
def parse_dri
|
|
@@ -156,18 +207,20 @@ module PureJPEG
|
|
|
156
207
|
# Extract entropy-coded scan data (everything from current position to EOI marker).
|
|
157
208
|
def extract_scan_data
|
|
158
209
|
start = @pos
|
|
210
|
+
len = @data.bytesize
|
|
159
211
|
# Scan forward looking for a marker that isn't a stuffing byte or restart
|
|
160
|
-
while @pos <
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
212
|
+
while @pos < len - 1
|
|
213
|
+
found = @data.index("\xFF".b, @pos)
|
|
214
|
+
break unless found && found < len - 1
|
|
215
|
+
@pos = found
|
|
216
|
+
next_byte = @data.getbyte(@pos + 1)
|
|
217
|
+
# 0x00 is byte stuffing, 0xD0-0xD7 are restart markers, 0xFF is padding — all part of scan data
|
|
218
|
+
if next_byte != 0x00 && !(next_byte >= 0xD0 && next_byte <= 0xD7) && next_byte != 0xFF
|
|
219
|
+
break
|
|
167
220
|
end
|
|
168
|
-
@pos +=
|
|
221
|
+
@pos += 2
|
|
169
222
|
end
|
|
170
|
-
@
|
|
223
|
+
@data[start...@pos]
|
|
171
224
|
end
|
|
172
225
|
end
|
|
173
226
|
end
|
|
@@ -19,22 +19,25 @@ module PureJPEG
|
|
|
19
19
|
|
|
20
20
|
# @param image [ChunkyPNG::Image] the source PNG image
|
|
21
21
|
def initialize(image)
|
|
22
|
-
@image = image
|
|
23
22
|
@width = image.width
|
|
24
23
|
@height = image.height
|
|
24
|
+
@packed_pixels = image.pixels
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
# @return [Array<Integer>] flat row-major array of packed RGBA integers
|
|
28
|
+
attr_reader :packed_pixels
|
|
29
|
+
|
|
27
30
|
# Retrieve a pixel at the given coordinate.
|
|
28
31
|
#
|
|
29
32
|
# @param x [Integer] column (0-based)
|
|
30
33
|
# @param y [Integer] row (0-based)
|
|
31
34
|
# @return [Pixel]
|
|
32
35
|
def [](x, y)
|
|
33
|
-
color = @
|
|
36
|
+
color = @packed_pixels[y * @width + x]
|
|
34
37
|
Pixel.new(
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
(color >> 24) & 0xFF,
|
|
39
|
+
(color >> 16) & 0xFF,
|
|
40
|
+
(color >> 8) & 0xFF
|
|
38
41
|
)
|
|
39
42
|
end
|
|
40
43
|
end
|
data/lib/pure_jpeg/version.rb
CHANGED
data/lib/pure_jpeg.rb
CHANGED
|
@@ -16,6 +16,7 @@ require_relative "pure_jpeg/huffman/encoder"
|
|
|
16
16
|
require_relative "pure_jpeg/huffman/decoder"
|
|
17
17
|
require_relative "pure_jpeg/jfif_writer"
|
|
18
18
|
require_relative "pure_jpeg/jfif_reader"
|
|
19
|
+
require_relative "pure_jpeg/info"
|
|
19
20
|
require_relative "pure_jpeg/image"
|
|
20
21
|
require_relative "pure_jpeg/encoder"
|
|
21
22
|
require_relative "pure_jpeg/decoder"
|
|
@@ -25,6 +26,12 @@ require_relative "pure_jpeg/decoder"
|
|
|
25
26
|
# Supports baseline DCT (SOF0) with 8-bit precision, grayscale and YCbCr
|
|
26
27
|
# color (4:2:0 chroma subsampling), and standard Huffman tables (Annex K).
|
|
27
28
|
module PureJPEG
|
|
29
|
+
# Raised when decoding invalid or unsupported JPEG data.
|
|
30
|
+
class DecodeError < StandardError; end
|
|
31
|
+
|
|
32
|
+
# Maximum image dimension (width or height) allowed for encoding and decoding.
|
|
33
|
+
MAX_DIMENSION = 8192
|
|
34
|
+
|
|
28
35
|
# Encode a pixel source as a JPEG.
|
|
29
36
|
#
|
|
30
37
|
# @param source [#width, #height, #[]] any object responding to +width+,
|
|
@@ -57,4 +64,27 @@ module PureJPEG
|
|
|
57
64
|
def self.read(path_or_data)
|
|
58
65
|
Decoder.decode(path_or_data)
|
|
59
66
|
end
|
|
67
|
+
|
|
68
|
+
# Read JPEG dimensions and basic frame metadata without decoding scan data.
|
|
69
|
+
#
|
|
70
|
+
# @param path_or_data [String] a file path or raw JPEG bytes
|
|
71
|
+
# @return [Info] image metadata parsed from the frame header
|
|
72
|
+
def self.info(path_or_data)
|
|
73
|
+
data = if path_or_data.is_a?(String) && !path_or_data.start_with?("\xFF\xD8".b) && File.exist?(path_or_data)
|
|
74
|
+
File.binread(path_or_data)
|
|
75
|
+
else
|
|
76
|
+
path_or_data.b
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
jfif = JFIFReader.new(data, stop_after_frame: true)
|
|
80
|
+
raise DecodeError, "JPEG frame header not found" unless jfif.width && jfif.height
|
|
81
|
+
|
|
82
|
+
Info.new(
|
|
83
|
+
width: jfif.width,
|
|
84
|
+
height: jfif.height,
|
|
85
|
+
component_count: jfif.components.length,
|
|
86
|
+
progressive: jfif.progressive,
|
|
87
|
+
icc_profile: jfif.icc_profile
|
|
88
|
+
)
|
|
89
|
+
end
|
|
60
90
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pure_jpeg
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Peter Cooper
|
|
@@ -57,6 +57,7 @@ files:
|
|
|
57
57
|
- lib/pure_jpeg/huffman/encoder.rb
|
|
58
58
|
- lib/pure_jpeg/huffman/tables.rb
|
|
59
59
|
- lib/pure_jpeg/image.rb
|
|
60
|
+
- lib/pure_jpeg/info.rb
|
|
60
61
|
- lib/pure_jpeg/jfif_reader.rb
|
|
61
62
|
- lib/pure_jpeg/jfif_writer.rb
|
|
62
63
|
- lib/pure_jpeg/quantization.rb
|
|
@@ -78,7 +79,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
78
79
|
requirements:
|
|
79
80
|
- - ">="
|
|
80
81
|
- !ruby/object:Gem::Version
|
|
81
|
-
version:
|
|
82
|
+
version: 3.0.0
|
|
82
83
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
83
84
|
requirements:
|
|
84
85
|
- - ">="
|