Skip to content

Commit abd540e

Browse files
authored
feat(decode): support IO and in-memory data (#13)
1 parent 8de1547 commit abd540e

File tree

7 files changed

+102
-42
lines changed

7 files changed

+102
-42
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,16 @@ jpeg_encode(img; kwargs...) -> Vector{UInt8}
2626

2727
```julia
2828
jpeg_decode([T,] filename::AbstractString; kwargs...) -> Matrix{T}
29+
jpeg_decode([T,] io::IO; kwargs...) -> Matrix{T}
30+
jpeg_decode([T,] data::Vector{UInt8}; kwargs...) -> Matrix{T}
2931
```
3032

3133
## Feature set
3234

3335
| function | filename | IOStream | in-memory buffer | pre-allocated output | multi-threads |
3436
| -------------------- | -------- | -------- | -------------------- | ------------------- | ------------- |
3537
| `jpeg_encode` | x | x | x | | x |
36-
| `jpeg_decode` | x | | | | x |
38+
| `jpeg_decode` | x | x | x | | x |
3739
| `ImageMagick.save` | x | x | x | | x |
3840
| `ImageMagick.load` | x | x | x | | x |
3941
| `QuartzImageIO.save` | x | x | x (`FileIO.Stream`) | | x |

docs/src/index.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,16 @@ jpeg_encode(img; kwargs...) -> Vector{UInt8}
2121

2222
```julia
2323
jpeg_decode([T,] filename::AbstractString; kwargs...) -> Matrix{T}
24+
jpeg_decode([T,] io::IO; kwargs...) -> Matrix{T}
25+
jpeg_decode([T,] data::Vector{UInt8}; kwargs...) -> Matrix{T}
2426
```
2527

2628
## Feature set
2729

2830
| function | filename | IOStream | in-memory buffer | pre-allocated output | multi-threads |
2931
| -------------------- | -------- | -------- | -------------------- | ------------------- | ------------- |
3032
| `jpeg_encode` | x | x | x | | x |
31-
| `jpeg_decode` | x | | | | x |
33+
| `jpeg_decode` | x | x | x | | x |
3234
| `ImageMagick.save` | x | x | x | | x |
3335
| `ImageMagick.load` | x | x | x | | x |
3436
| `QuartzImageIO.save` | x | x | x (`FileIO.Stream`) | | x |

src/decode.jl

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
"""
22
jpeg_decode([T,] filename::AbstractString; kwargs...) -> Matrix{T}
3+
jpeg_decode([T,] io::IO; kwargs...) -> Matrix{T}
4+
jpeg_decode([T,] data::Vector{UInt8}; kwargs...) -> Matrix{T}
35
4-
Decode the JPEG image from given I/O stream as colorant matrix.
6+
Decode the JPEG image as colorant matrix. The source data can be either a filename, an IO
7+
, or an in-memory byte sequence.
58
69
# parameters
710
@@ -47,11 +50,10 @@ filename = testimage("earth", download_only=true)
4750
"""
4851
function jpeg_decode(
4952
::Type{CT},
50-
filename::AbstractString;
53+
data::Vector{UInt8};
5154
transpose=false,
5255
scale_ratio=1) where CT<:Colorant
53-
infile = ccall(:fopen, Libc.FILE, (Cstring, Cstring), filename, "rb")
54-
@assert infile.ptr != C_NULL
56+
_jpeg_check_bytes(data)
5557
out_CT, jpeg_cls = _jpeg_out_color_space(CT)
5658

5759
cinfo_ref = Ref(LibJpeg.jpeg_decompress_struct())
@@ -60,7 +62,7 @@ function jpeg_decode(
6062
cinfo = cinfo_ref[]
6163
cinfo.err = LibJpeg.jpeg_std_error(jerr)
6264
LibJpeg.jpeg_create_decompress(cinfo_ref)
63-
LibJpeg.jpeg_stdio_src(cinfo_ref, infile)
65+
LibJpeg.jpeg_mem_src(cinfo_ref, data, length(data))
6466
LibJpeg.jpeg_read_header(cinfo_ref, true)
6567

6668
# set decompression parameters, if given
@@ -87,13 +89,21 @@ function jpeg_decode(
8789
end
8890
finally
8991
LibJpeg.jpeg_destroy_decompress(cinfo_ref)
90-
ccall(:fclose, Cint, (Ptr{Libc.FILE},), infile)
9192
end
9293
end
93-
function jpeg_decode(filename::AbstractString; kwargs...)
94-
return jpeg_decode(_default_out_color_space(filename), filename; kwargs...)
94+
jpeg_decode(data; kwargs...) = jpeg_decode(_default_out_color_space(data), data; kwargs...)
95+
96+
# TODO(johnnychen94): support Progressive JPEG
97+
# TODO(johnnychen94): support partial decoding
98+
function jpeg_decode(::Type{CT}, filename::AbstractString; kwargs...) where CT<:Colorant
99+
open(filename, "r") do io
100+
jpeg_decode(CT, io; kwargs...)
101+
end
95102
end
96103

104+
jpeg_decode(io::IO; kwargs...) = jpeg_decode(read(io); kwargs...)
105+
jpeg_decode(::Type{CT}, io::IO; kwargs...) where CT<:Colorant = jpeg_decode(CT, read(io); kwargs...)
106+
97107
function _jpeg_decode!(out::Matrix{<:Colorant}, cinfo_ref::Ref{LibJpeg.jpeg_decompress_struct})
98108
row_stride = size(out, 1) * length(eltype(out))
99109
buf = Vector{UInt8}(undef, row_stride)
@@ -120,6 +130,7 @@ const _allowed_scale_ratios = ntuple(i->i//8, 16)
120130
_cal_scale_ratio(r::Real) = _allowed_scale_ratios[findmin(x->abs(x-r), _allowed_scale_ratios)[2]]
121131

122132
function _default_out_color_space(filename::AbstractString)
133+
_jpeg_check_bytes(filename)
123134
infile = ccall(:fopen, Libc.FILE, (Cstring, Cstring), filename, "rb")
124135
@assert infile.ptr != C_NULL
125136
cinfo_ref = Ref(LibJpeg.jpeg_decompress_struct())
@@ -137,6 +148,22 @@ function _default_out_color_space(filename::AbstractString)
137148
end
138149
end
139150

151+
function _default_out_color_space(data::Vector{UInt8})
152+
_jpeg_check_bytes(data)
153+
cinfo_ref = Ref(LibJpeg.jpeg_decompress_struct())
154+
try
155+
jerr = Ref{LibJpeg.jpeg_error_mgr}()
156+
cinfo_ref[].err = LibJpeg.jpeg_std_error(jerr)
157+
LibJpeg.jpeg_create_decompress(cinfo_ref)
158+
LibJpeg.jpeg_mem_src(cinfo_ref, data, length(data))
159+
LibJpeg.jpeg_read_header(cinfo_ref, true)
160+
LibJpeg.jpeg_calc_output_dimensions(cinfo_ref)
161+
return jpeg_color_space(cinfo_ref[].out_color_space)
162+
finally
163+
LibJpeg.jpeg_destroy_decompress(cinfo_ref)
164+
end
165+
end
166+
140167
function _jpeg_out_color_space(::Type{CT}) where CT
141168
try
142169
n0f8(CT), jpeg_color_space(n0f8(CT))
@@ -145,3 +172,25 @@ function _jpeg_out_color_space(::Type{CT}) where CT
145172
RGB{N0f8}, jpeg_color_space(RGB{N0f8})
146173
end
147174
end
175+
176+
# provides some basic integrity check
177+
# TODO(johnnychen94): redirect libjpeg-turbo error to julia
178+
_jpeg_check_bytes(filename::AbstractString) = open(_jpeg_check_bytes, filename, "r")
179+
function _jpeg_check_bytes(io::IO)
180+
seekend(io)
181+
nbytes = position(io)
182+
nbytes > 623 || throw(ArgumentError("Invalid number of bytes."))
183+
184+
buf = UInt8[]
185+
seekstart(io)
186+
readbytes!(io, buf, 623)
187+
seek(io, nbytes-2)
188+
append!(buf, read(io, 2))
189+
return _jpeg_check_bytes(buf)
190+
end
191+
function _jpeg_check_bytes(data::Vector{UInt8})
192+
length(data) > 623 || throw(ArgumentError("Invalid number of bytes."))
193+
data[1:2] == [0xff, 0xd8] || throw(ArgumentError("Invalid JPEG byte sequence."))
194+
data[end-1:end] == [0xff, 0xd9] || @warn "Premature end of JPEG byte sequence."
195+
return true
196+
end

test/Project.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595"
33
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
44
ImageCore = "a09fc81d-aa75-5fe9-8630-4744c3626534"
55
ImageIO = "82e4d734-157c-48bb-816b-45c225c6df19"
6-
ImageMagick = "6218d12a-5da1-5696-b52f-db25d2ecc6d1"
76
ImageQualityIndexes = "2996bd0c-7a13-11e9-2da2-2f5ce47296a9"
87
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
98
TestImages = "5e47fb64-e119-507b-a336-dd2b206d9990"

test/runtests.jl

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,12 @@ using Aqua
55
using Documenter
66
using TestImages
77
using ImageQualityIndexes
8-
using ImageMagick
98
using ImageCore
109

1110
# ensure TestImages artifacts are downloaded before running documenter test
1211
testimage("cameraman")
1312

14-
tmpdir = tempdir()
15-
function decode_encode(img; kwargs...)
16-
tmpfile = joinpath(tmpdir, "tmp.jpg")
17-
buf = @inferred jpeg_encode(img; kwargs...)
18-
write(tmpfile, buf)
19-
return jpeg_decode(tmpfile)
20-
end
13+
const tmpdir = tempdir()
2114

2215
@testset "JpegTurbo.jl" begin
2316
if !Sys.iswindows() # DEBUG

test/tst_decode.jl

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,38 @@
11
@testset "jpeg_decode" begin
22
img_rgb = testimage("lighthouse")
3-
4-
tmpfile = joinpath(tmpdir, "tmp.jpg")
5-
jpeg_encode(tmpfile, img_rgb)
6-
7-
data = jpeg_decode(tmpfile)
8-
9-
@test jpeg_decode(tmpfile; transpose=true) == data'
3+
img_rgb_bytes = jpeg_encode(img_rgb)
104

115
# ensure default keyword values are not changed by accident
12-
@test jpeg_decode(tmpfile)
13-
jpeg_decode(RGB, tmpfile; transpose=false, scale_ratio=1)
14-
jpeg_decode(tmpfile; transpose=false, scale_ratio=1)
6+
@test jpeg_decode(img_rgb_bytes)
7+
jpeg_decode(RGB, img_rgb_bytes; transpose=false, scale_ratio=1)
8+
jpeg_decode(img_rgb_bytes; transpose=false, scale_ratio=1)
9+
10+
@testset "filename and IOStream" begin
11+
tmpfile = joinpath(tmpdir, "tmp.jpg")
12+
jpeg_encode(tmpfile, img_rgb)
13+
@test read(tmpfile) == img_rgb_bytes
14+
15+
# IOStream
16+
img = open(tmpfile, "r") do io
17+
jpeg_decode(io)
18+
end
19+
@test img == jpeg_decode(img_rgb_bytes)
1520

21+
img = open(tmpfile, "r") do io
22+
jpeg_decode(Gray, io; scale_ratio=0.5)
23+
end
24+
@test img == jpeg_decode(Gray, img_rgb_bytes; scale_ratio=0.5)
1625

17-
# TODO(johnnychen94): support IO and in-memory buffer
18-
@test_broken jpeg_decode(jpeg_encode(img_rgb))
19-
@test_broken open(jpeg_decode, tmpfile, "r")
26+
# filename
27+
@test jpeg_decode(tmpfile) == jpeg_decode(img_rgb_bytes)
28+
@test jpeg_decode(Gray, tmpfile; scale_ratio=0.5) == jpeg_decode(Gray, img_rgb_bytes; scale_ratio=0.5)
29+
end
2030

2131
@testset "colorspace" begin
2232
native_color_spaces = [Gray, RGB, BGR, RGBA, BGRA, ABGR, ARGB]
2333
ext_color_spaces = [YCbCr, RGBX, XRGB, Lab, YIQ] # supported by Colors.jl
2434
for CT in [native_color_spaces..., ext_color_spaces...]
25-
data = jpeg_decode(CT, tmpfile)
35+
data = jpeg_decode(CT, img_rgb_bytes)
2636
@test eltype(data) <: CT
2737
if CT == Gray
2838
@test assess_psnr(data, Gray.(img_rgb)) > 34.92
@@ -33,17 +43,21 @@
3343
end
3444

3545
@testset "scale_ratio" begin
36-
data = jpeg_decode(tmpfile; scale_ratio=0.25)
46+
data = jpeg_decode(img_rgb_bytes; scale_ratio=0.25)
3747
@test size(data) == (128, 192) == 0.25 .* size(img_rgb)
3848

3949
# `jpeg_decode` will map input `scale_ratio` to allowed values.
40-
data = jpeg_decode(tmpfile; scale_ratio=0.3)
50+
data = jpeg_decode(img_rgb_bytes; scale_ratio=0.3)
4151
@test size(data) == (128, 192) != 0.3 .* size(img_rgb)
4252
end
4353

4454
@testset "transpose" begin
45-
jpeg_encode(tmpfile, img_rgb; transpose=true)
46-
data = jpeg_decode(tmpfile; transpose=true)
55+
data = jpeg_decode(jpeg_encode(img_rgb; transpose=true); transpose=true)
4756
@test assess_psnr(data, img_rgb) > 33.95
4857
end
58+
59+
@testset "integrity check" begin
60+
@test_throws ArgumentError jpeg_decode(UInt8[])
61+
@test_throws ArgumentError jpeg_decode(img_rgb_bytes[1:600])
62+
end
4963
end

test/tst_encode.jl

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@ img_rgb = testimage("lighthouse")
55
@testset "basic" begin
66
for CT in [Gray, RGB, #=YCbCr,=# #=RGBX,=# BGR, #=XRGB,=# RGBA, BGRA, ABGR, ARGB]
77
img = CT.(img_rgb)
8-
data = decode_encode(img)
8+
data = jpeg_decode(jpeg_encode(img))
99
@test eltype(data) <: Union{Gray, RGB}
1010
@test size(data) == size(img)
11-
@test data decode_encode(float32.(img))
11+
@test data jpeg_decode(jpeg_encode(float32.(img)))
1212

1313
# ensure default keyword values are not changed by accident
14-
@test data == decode_encode(img, transpose=false)
15-
@test decode_encode(img, transpose=true) == decode_encode(img', transpose=false)
14+
@test data == jpeg_decode(jpeg_encode(img, transpose=false))
15+
@test jpeg_decode(jpeg_encode(img, transpose=true)) ==
16+
jpeg_decode(jpeg_encode(img', transpose=false))
1617
end
1718

1819
# numerical array is treated as Gray image
@@ -35,7 +36,7 @@ end
3536
100 => 59.31,
3637
]
3738
for (q, r) in psnr_refs
38-
v = assess_psnr(img, decode_encode(img, quality=q))
39+
v = assess_psnr(img, jpeg_decode(jpeg_encode(img, quality=q)))
3940
@test v >= r
4041
end
4142
end

0 commit comments

Comments
 (0)