Skip to content

Commit 4cdb22f

Browse files
committed
feat: initial jpeg_encode implementation
Following example.txt and libjpeg.txt in libjpeg-turbo, this commit implements a `jpeg_encode` function with two APIs: - `jpeg_encode(io_or_filename, img; kwargs...) -> Int` - `jpeg_encode(img; kwargs...) -> Vector{UInt8}`
1 parent 013210c commit 4cdb22f

File tree

8 files changed

+220
-0
lines changed

8 files changed

+220
-0
lines changed

Project.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ version = "0.1.0"
55

66
[deps]
77
CEnum = "fa961155-64e5-5f13-b03f-caf6b980ea82"
8+
ImageCore = "a09fc81d-aa75-5fe9-8630-4744c3626534"
89
JpegTurbo_jll = "aacddb02-875f-59d6-b918-886e6ef4fbf8"
910

1011
[compat]
1112
CEnum = "0.3, 0.4"
13+
ImageCore = "0.8, 0.9"
1214
JpegTurbo_jll = "~2.1"
1315
julia = "1.6"

docs/src/reference.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ CurrentModule = JpegTurbo
44

55
# Function references
66

7+
```@docs
8+
jpeg_encode
9+
```
10+
711
## Utilities
812

913
```@docs

src/JpegTurbo.jl

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
module JpegTurbo
22

3+
using ImageCore
4+
35
include("../lib/LibJpeg.jl")
46
using .LibJpeg
57
include("libjpeg_utils.jl")
68

9+
include("common.jl")
10+
include("encode.jl")
11+
12+
export jpeg_encode
13+
714
end

src/common.jl

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
jpeg_components(::AbstractArray{T}) where T = jpeg_components(T)
2+
jpeg_components(::Type{CT}) where CT<:Colorant = length(CT)
3+
jpeg_components(::Type{T}) where T<:Number = 1
4+
5+
jpeg_color_space(::AbstractArray{T}) where T = jpeg_color_space(T)
6+
jpeg_color_space(::Type{CT}) where CT<:Gray = LibJpeg.JCS_GRAYSCALE
7+
jpeg_color_space(::Type{CT}) where CT<:RGB = LibJpeg.JCS_RGB
8+
jpeg_color_space(::Type{CT}) where CT<:BGR = LibJpeg.JCS_EXT_BGR
9+
jpeg_color_space(::Type{CT}) where CT<:RGBA = LibJpeg.JCS_EXT_RGBA
10+
jpeg_color_space(::Type{CT}) where CT<:BGRA = LibJpeg.JCS_EXT_BGRA
11+
jpeg_color_space(::Type{CT}) where CT<:ABGR = LibJpeg.JCS_EXT_ABGR
12+
jpeg_color_space(::Type{CT}) where CT<:ARGB = LibJpeg.JCS_EXT_ARGB

src/encode.jl

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
"""
2+
jpeg_encode(filename::AbstractString, img; kwargs...) -> Int
3+
jpeg_encode(io::IO, img; kwargs...) -> Int
4+
jpeg_encode(img; kwargs...) -> Vector{UInt8}
5+
6+
Encode 2D image `img` as JPEG byte sequences and write to given I/O stream or file. The
7+
return value is number of bytes. If output is not specified, the encoded result is stored
8+
in memory as return value.
9+
10+
# Parameters
11+
12+
- `transpose::Bool`: whether we need to permute the image's width and height dimension
13+
before encoding. The default value is `false`.
14+
- `quality::Int`: Constructs JPEG quantization tables appropriate for the indicated
15+
quality setting. The quality value is expressed on the 0..100 scale recommended by IJG.
16+
17+
!!! info "Custom compression parameters"
18+
JPEG has a large number of compression parameters that determine how the image is
19+
encoded. Most applications don't need or want to know about all these parameters. For
20+
more detailed information and explaination, please refer to the "Compression parameter
21+
selection" in [1]. Unsupported custom parameters might cause Julia segmentation fault.
22+
23+
# Examples
24+
25+
```jldoctest
26+
julia> using JpegTurbo, TestImages
27+
28+
julia> img = testimage("cameraman");
29+
30+
julia> jpeg_encode("out.jpg", img) # write to file
31+
28210
32+
33+
julia> buf = jpeg_encode(img); length(buf) # directly write to memory
34+
28210
35+
```
36+
37+
# References
38+
39+
- [1] [libjpeg API Documentation (libjpeg.txt)](https://raw.githubusercontent.com/libjpeg-turbo/libjpeg-turbo/main/libjpeg.txt)
40+
"""
41+
function jpeg_encode(img::AbstractMatrix; transpose=false, kwargs...)
42+
# quantilized into 8bit sequences first
43+
AT = Array{n0f8(eltype(img)), ndims(img)}
44+
# jpegturbo is a C library and assumes row-major memory order, thus `collect` the data into
45+
# contiguous memeory layout already makes a transpose.
46+
img = transpose ? convert(AT, img) : convert(AT, PermutedDimsArray(img, (2, 1)))
47+
48+
return _encode(img; kwargs...)
49+
end
50+
51+
function jpeg_encode(filename::AbstractString, img; kwargs...)
52+
open(filename, "w") do io
53+
jpeg_encode(io, img; kwargs...)
54+
end
55+
end
56+
# TODO(johnnychen94): further improve the performance via asynchronously IO and buffer reuse.
57+
jpeg_encode(io::IO, img; kwargs...) = write(io, jpeg_encode(img; kwargs...))
58+
59+
60+
function _encode(
61+
img::Matrix{<:Colorant};
62+
colorspace::Union{Nothing,Type} = nothing,
63+
quality::Union{Nothing,Int} = nothing,
64+
arith_code::Union{Nothing,Bool} = nothing,
65+
optimize_coding::Union{Nothing,Bool} = nothing,
66+
smoothing_factor::Union{Nothing,Int} = nothing,
67+
write_JFIF_header::Union{Nothing,Bool} = nothing,
68+
JFIF_version::Union{Nothing,VersionNumber} = nothing,
69+
density_unit::Union{Nothing,Int} = nothing,
70+
X_density::Union{Nothing,Int} = nothing,
71+
Y_density::Union{Nothing,Int} = nothing,
72+
write_Adobe_marker::Union{Nothing,Bool} = nothing
73+
)
74+
cinfo = LibJpeg.jpeg_compress_struct()
75+
cinfo_ref = Ref(cinfo)
76+
jerr = Ref{LibJpeg.jpeg_error_mgr}()
77+
cinfo.err = LibJpeg.jpeg_std_error(jerr)
78+
LibJpeg.jpeg_create_compress(cinfo_ref)
79+
80+
# set input image information
81+
cinfo.image_width = size(img, 1)
82+
cinfo.image_height = size(img, 2)
83+
cinfo.input_components = jpeg_components(img)
84+
cinfo.in_color_space = jpeg_color_space(img)
85+
86+
# set compression keywords
87+
# it's recommended to call `jpeg_set_defaults` first before setting custom parameters
88+
# as it's more likely to provide a working parameters and is more likely to be working
89+
# correctly in the future.
90+
LibJpeg.jpeg_set_defaults(cinfo_ref)
91+
isnothing(colorspace) || LibJpeg.jpeg_set_colorspace(cinfo_ref, jpeg_color_space(colorspace))
92+
isnothing(quality) || LibJpeg.jpeg_set_quality(cinfo_ref, quality, true)
93+
isnothing(arith_code) || (cinfo.arith_code = arith_code)
94+
isnothing(optimize_coding) || (cinfo.optimize_coding = optimize_coding)
95+
isnothing(smoothing_factor) || (cinfo.smoothing_factor = smoothing_factor)
96+
isnothing(write_JFIF_header) || (cinfo.write_JFIF_header = write_JFIF_header)
97+
if !isnothing(JFIF_version)
98+
cinfo.JFIF_major_version = UInt8(JFIF_version.major)
99+
cinfo.JFIF_minor_version = UInt8(JFIF_version.minor)
100+
end
101+
isnothing(density_unit) || (cinfo.density_unit = density_unit)
102+
isnothing(X_density) || (cinfo.X_density = X_density)
103+
isnothing(Y_density) || (cinfo.Y_density = Y_density)
104+
isnothing(write_Adobe_marker) || (cinfo.write_Adobe_marker = write_Adobe_marker)
105+
106+
# set destination
107+
# TODO(johnnychen94): allow pre-allocated buffer
108+
bufsize = Ref{Culong}(0)
109+
buf_ptr = Ref{Ptr{UInt8}}(C_NULL)
110+
LibJpeg.jpeg_mem_dest(cinfo_ref, buf_ptr, bufsize)
111+
112+
# compression stage
113+
LibJpeg.jpeg_start_compress(cinfo_ref, true)
114+
row_stride = size(img, 1) * jpeg_components(img)
115+
row_pointer = Ref{Ptr{UInt8}}(0)
116+
while (cinfo.next_scanline < cinfo.image_height)
117+
row_pointer[] = pointer(img) + cinfo.next_scanline * row_stride
118+
LibJpeg.jpeg_write_scanlines(cinfo_ref, row_pointer, 1);
119+
end
120+
LibJpeg.jpeg_finish_compress(cinfo_ref)
121+
LibJpeg.jpeg_destroy_compress(cinfo_ref)
122+
123+
return unsafe_wrap(Array, buf_ptr[], bufsize[]; own=true)
124+
end

test/Project.toml

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

test/runtests.jl

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,34 @@
11
using JpegTurbo
22
using JpegTurbo.LibJpeg
33
using Test
4+
using Aqua
5+
using Documenter
6+
using TestImages
7+
using ImageQualityIndexes
8+
using ImageMagick
9+
using ImageCore
10+
11+
tmpdir = tempdir()
12+
function decode_encode(img; kwargs...)
13+
tmpfile = joinpath(tmpdir, "tmp.jpg")
14+
buf = @inferred jpeg_encode(img; kwargs...)
15+
write(tmpfile, buf)
16+
# TODO(johnnychen94): load back with `JpegTurbo.decode`
17+
return ImageMagick.load(tmpfile)
18+
end
419

520
@testset "JpegTurbo.jl" begin
21+
@testset "Project meta quality checks" begin
22+
Aqua.test_all(JpegTurbo;
23+
ambiguities=false,
24+
project_extras=true,
25+
deps_compat=true,
26+
stale_deps=true,
27+
project_toml_formatting=true
28+
)
29+
doctest(JpegTurbo, manual = false)
30+
end
31+
632
@testset "config" begin
733
@test_nowarn JpegTurbo.versioninfo()
834

@@ -14,5 +40,11 @@ using Test
1440
@test LibJpeg.BITS_IN_JSAMPLE == 8
1541
@test LibJpeg.MAXJSAMPLE == 255
1642
@test LibJpeg.CENTERJSAMPLE == 128
43+
44+
# ensure colorspace extensions are supported
45+
@test LibJpeg.JCS_EXTENSIONS == 1
46+
@test LibJpeg.JCS_ALPHA_EXTENSIONS == 1
1747
end
48+
49+
include("tst_encode.jl")
1850
end

test/tst_encode.jl

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
@testset "jpeg_encode" begin
2+
3+
img_rgb = testimage("lighthouse")
4+
5+
@testset "basic" begin
6+
for CT in [Gray, RGB, #=YCbCr,=# #=RGBX,=# BGR, #=XRGB,=# RGBA, BGRA, ABGR, ARGB]
7+
img = CT.(img_rgb)
8+
data = decode_encode(img)
9+
@test data decode_encode(float32.(img))
10+
11+
# ensure default keyword values are not changed by accident
12+
@test data == decode_encode(img, transpose=false)
13+
@test decode_encode(img, transpose=true) == decode_encode(img', transpose=false)
14+
end
15+
end
16+
17+
# keyword checks
18+
@testset "quality" begin
19+
img = testimage("cameraman")
20+
psnr_refs = Dict(
21+
1 => 24.5647,
22+
10 => 31.3434,
23+
50 => 38.8765,
24+
100 => 59.3770,
25+
)
26+
for (q, r) in psnr_refs
27+
v = assess_psnr(img, decode_encode(img, quality=q))
28+
@test v >= r
29+
end
30+
end
31+
32+
end # @testset "jpeg_encode"

0 commit comments

Comments
 (0)