Skip to content

Commit

Permalink
add decoder
Browse files Browse the repository at this point in the history
  • Loading branch information
johnnychen94 committed May 12, 2021
1 parent 607847e commit 86fdae9
Show file tree
Hide file tree
Showing 9 changed files with 260 additions and 38 deletions.
6 changes: 4 additions & 2 deletions src/Sixel.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ module Sixel
export sixel_encode, sixel_decode

using ImageCore
using IndirectArrays # sixel sequence is actually an indexed image format

import REPL: Terminals

include("interface.jl")
Expand All @@ -19,7 +21,7 @@ using .LibSixel

# Eventually we will rewrite everything in pure Julia :)
default_encoder(::AbstractArray) = LibSixel.LibSixelEncoder()
# default_decoder(::AbstractArray) = LibSIxel.LibSixelDecoder()
default_decoder() = LibSixel.LibSixelDecoder()


# The high-level API to deal with different Julia input types.
Expand All @@ -28,7 +30,7 @@ default_encoder(::AbstractArray) = LibSixel.LibSixelEncoder()
# fancy array types (e.g., transpose, view) or lazy generators, if supported, should be handled here
# instead of in the backends.
include("encoder.jl")
# include("decoder.jl)
include("decoder.jl")


# Ref: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
Expand Down
34 changes: 18 additions & 16 deletions src/backend/libsixel/LibSixel.jl
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,21 @@
# `sixel_decode`/`sixel_decode_raw`

module LibSixel
export LibSixelEncoder

using ..SixelInterface
import ..SixelInterface: canonical_sixel_eltype

# This file is auto-generated by Clang.jl and is not expected to be modified directly.
# However, there're still some methods are modified:
# - `sixel_output_new`: ccall argtype for `priv` is relaxed from `Ptr{Cvoid}` to `Any`
include("CModule.jl")
using .C
include("types.jl")

using ImageCore
include("encoder.jl") # high-level encoder API
# include("decoder.jl") # high-level decoder API
end

export LibSixelEncoder, LibSixelDecoder

using ..SixelInterface
import ..SixelInterface: canonical_sixel_eltype

# This file is auto-generated by Clang.jl and is not expected to be modified directly.
# However, there're still some methods are modified:
# - `sixel_output_new`: ccall argtype for `priv` is relaxed from `Ptr{Cvoid}` to `Any`
include("CModule.jl")
using .C
include("types.jl")

using ImageCore, OffsetArrays
include("encoder.jl") # high-level encoder API
include("decoder.jl") # high-level decoder API

end # module
38 changes: 38 additions & 0 deletions src/backend/libsixel/decoder.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
struct LibSixelDecoder <: AbstractSixelDecoder
# (Experimental) internal fields

# We need `allocator` to constructor libsixel objects, however, users of this
# Julia package is not expected to use this field as it really should just live
# in the C world.
allocator::SixelAllocator

function LibSixelDecoder(allocator=SixelAllocator())
new(allocator)
end
end

function (dec::LibSixelDecoder)(bytes::Vector{UInt8}; transpose=false)
pixels = Ref{Ptr{Cuchar}}()
palette = Ref{Ptr{Cuchar}}()
pwidth = Ref(Cint(0))
pheight = Ref(Cint(0))
ncolors = Ref(Cint(0))

status = C.sixel_decode_raw(
bytes, length(bytes),
pixels, pwidth, pheight,
palette, ncolors,
dec.allocator.ptr
)
check_status(status)

index = unsafe_wrap(Matrix{Cuchar}, pixels[], (pwidth[], pheight[]); own=false)

# libsixel declares it to be ARGB but it's actually RGB{N0f8}
PT = RGB{N0f8}
pvalues = convert(Ptr{PT}, palette[])
values = unsafe_wrap(Vector{PT}, pvalues, (ncolors[], ); own=false)
values = OffsetArray(values, OffsetArrays.Origin(0)) # again, libsixel assumes 0-based indexing

return index, values
end
115 changes: 115 additions & 0 deletions src/decoder.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
"""
sixel_decode([T=RGB{N0f8}], src, [decoder]; kwargs...) -> img::IndirectArray
Decode the sixel format sequence provided by `src` and output as an indexed image.
# Arguments
- `T`: output eltype. By default it is `RGB{N0f8}`.
- `src`: the input sixel data source. It can be either an `IO`, `AbstractVector{UInt8}`, or `AbstractString`.
- `decoder::AbstractSixelDecoder`: the sixel decoder. Currently only `LibSixelDecoder` is available.
# Parameters
- `transpose::Bool`: whether we need to permute the image's width and height dimension before encoding.
The default value is `false`.
# References
- [1] VT330/VT340 Programmer Reference Manual, Volume 1: Text Programming
- [2] VT330/VT340 Programmer Reference Manual, Volume 2: Graphics Programming
- [3] https://github.com/saitoha/libsixel
"""
function sixel_decode end

sixel_decode(src, dec=default_decoder(); kwargs...) = sixel_decode(RGB{N0f8}, src, dec; kwargs...)

sixel_decode(::Type{T}, data::AbstractString, dec=default_decoder(); kwargs...) where T =
sixel_decode(T, collect(UInt8, convert(String, data)), dec; kwargs...)

function sixel_decode(::Type{T}, bytes::AbstractArray, dec=default_decoder(); transpose=false) where {T}
bytes = convert(Vector{UInt8}, bytes)

expected_size = read_sixel_size(bytes)
index, values = dec(bytes)
values = eltype(values) == T ? values : map(T, values)

if dec isa LibSixelDecoder
# Julia uses column-major order while libsixel uses row-major order,
# thus transpose=true means no permutation.
index = transpose ? index : PermutedDimsArray(index, (2, 1))
expected_size = transpose ? (expected_size[2], expected_size[1]) : expected_size
else
throw(ArgumentError("Unsupported decoder type $(typeof(enc)). Please open an issue for this."))
end

actual_size = size(index)
if expected_size != actual_size
@warn "Output size mismatch during decoding sixel sequences" actual_size expected_size
end
# We use IndirectArray to mark it as an indexed image so as to avoid unnecessary memory allocation
# Users that expect a dense Array can always call `collect(rst)` or `convert(Array, rst)` on this.
return IndirectArray(index, values)
end

function sixel_decode(::Type{T}, io::IO, dec=default_decoder(); kwargs...) where T
# TODO: This is actually a duplicated check since the bytes method also checks it
# but this is a quite fast operation...
expected_size = read_sixel_size(io)

bytes = read(io)
img = sixel_decode(T, bytes, dec; kwargs...)

actual_size = size(img)
if expected_size != actual_size
@warn "Output size mismatch during decoding sixel sequences" actual_size expected_size
end
return img
end


"""
read_sixel_size(io::IO)
read_sixel_size(bytes::Vector{UInt8})
Read the header from a sixel sequence `io`/`bytes` and return the size of the sixel image.
"""
function read_sixel_size(bytes::Vector{UInt8})
# There's absolutely something wrong if the header content exceeds 50 bytes
max_header_length = 50

p_end = findfirst(isequal(UInt8('#')), bytes)
p_end = isnothing(p_end) ? length(bytes) : p_end - 1
buffer = view(bytes, 1:min(p_end, max_header_length))
seps = findall(isequal(UInt8(';')), buffer)
length(seps) == 3 || throw(ArgumentError("The input data is not recognizable as sixel sequence."))
w = parse(Int, String(buffer[seps[2]+1:seps[3]-1]))
h = parse(Int, String(buffer[seps[3]+1:end]))
# (h, w) in column-major order convention
return h, w
end

function read_sixel_size(io::IO)
# Sixel sequence format always start with this
# \ePq"1;1;w;h#
# where `#` indicates the first palette value
buffer = UInt8[]
p = position(io)
try
# There's absolutely something wrong if the header content exceeds 50 bytes
i, max_header_length = 1, 50
ch = read(io, Cuchar)
# Cuchar('#') == 0x23
while ch != 0x23 && i < max_header_length
push!(buffer, ch)
ch = read(io, Cuchar)
i += 1
end
catch e
e isa EOFError && throw(ArgumentError("The input data is not recognizable as sixel sequence."))
rethrow(e)
finally
seek(io, p)
end
return read_sixel_size(buffer)
end
5 changes: 5 additions & 0 deletions src/encoder.jl
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ Encode colorant sequence `src` as sixel sequence and write it into a writable io
- `encoder::AbstractSixelEncoder`: the sixel encoder. Currently, only
[`LibSixelEncoder`](@ref Sixel.LibSixel.LibSixelEncoder) is available.
!!! warning
For better visualization quality, small image (e.g., vector) is repeated into a larger matrix.
Hence if you load the encoded small image back using [`sixel_decode`](@ref), the size will not
be the same.
# Parameters
- `transpose::Bool`: whether we need to permute the image's width and height dimension before encoding.
Expand Down
19 changes: 0 additions & 19 deletions src/interface.jl
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,6 @@ abstract type AbstractSixelEncoder end
abstract type AbstractSixelDecoder end
(enc::AbstractSixelDecoder)(io, src) = error("The decoder functor method for inputs (::$(typeof(io)), ::$(typeof(src)) is not implemented.")

"""
sixel_decode(io, src, [decoder]) -> io
Decode the sixel format sequence provided by `src` and write into a writable io-like object `io` as output.
# Arguments
- `io`: the output io-like object, which is expected to be writable.
- `src`: generic container object(e.g., `IO`, `AbstractArray`, `AbstractString`) that contains the sixel format sequence.
- `decoder::AbstractSixelDecoder`: the sixel decoder.
# References
- [1] VT330/VT340 Programmer Reference Manual, Volume 1: Text Programming
- [2] VT330/VT340 Programmer Reference Manual, Volume 2: Graphics Programming
- [3] https://github.com/saitoha/libsixel
"""
function sixel_decode(::Any, ::Any, ::AbstractSixelDecoder) end

"""
canonical_sixel_eltype(enc, CT1) -> CT2
Expand Down
2 changes: 2 additions & 0 deletions test/Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
ImageCore = "a09fc81d-aa75-5fe9-8630-4744c3626534"
ImageIO = "82e4d734-157c-48bb-816b-45c225c6df19"
ImageMagick = "6218d12a-5da1-5696-b52f-db25d2ecc6d1"
ImageQualityIndexes = "2996bd0c-7a13-11e9-2da2-2f5ce47296a9"
IndirectArrays = "9b13fd28-a010-5f03-acff-a1bbcff69959"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
TestImages = "5e47fb64-e119-507b-a336-dd2b206d9990"
76 changes: 76 additions & 0 deletions test/backend/libsixel.jl
Original file line number Diff line number Diff line change
Expand Up @@ -217,3 +217,79 @@
@test bufferdata == String(take!(io))
end
end

@testset "decoder" begin
@testset "API" begin
dec = Sixel.default_decoder()
@test dec isa Sixel.LibSixelDecoder

tmp_file = tempname()
img = repeat(Gray.(0:0.1:0.9), inner=(10, 50))
sz = size(img)
open(tmp_file, "w") do io
sixel_encode(io, img)
end

# IO
img_readback = open(tmp_file, "r") do io
sixel_decode(io)
end
@test sz == size(img_readback)
@test img_readback isa IndirectArray
@test eltype(img_readback) == RGB{N0f8}

img_readback = open(tmp_file, "r") do io
sixel_decode(io, dec)
end
@test sz == size(img_readback)
@test img_readback isa IndirectArray
@test eltype(img_readback) == RGB{N0f8}

img_readback = open(tmp_file, "r") do io
sixel_decode(Gray{N0f8}, io)
end
@test sz == size(img_readback)
@test img_readback isa IndirectArray
@test eltype(img_readback) == Gray{N0f8}

# String and Vector{UInt8}
bytes_data = open(tmp_file, "r") do io
read(io)
end
string_data = read(tmp_file, String)
for src in (bytes_data, string_data)
img_readback = sixel_decode(src, dec)
@test sz == size(img_readback)
@test img_readback isa IndirectArray
@test eltype(img_readback) == RGB{N0f8}

img_readback = sixel_decode(Gray{N0f8}, src, dec)
@test sz == size(img_readback)
@test img_readback isa IndirectArray
@test eltype(img_readback) == Gray{N0f8}
end
end

@testset "Quality test" begin
enc = Sixel.LibSixelEncoder()
dec = Sixel.LibSixelDecoder()
tmp_file = tempname()
for img in (
repeat(Gray.(0:0.1:0.9), inner=(10, 50)),
repeat(distinguishable_colors(10), inner=(10, 50)),
repeat(Gray.(0:0.1:0.9), inner=(10, 50, 3)),
repeat(distinguishable_colors(5), inner=(20, 50, 3))
)
open(tmp_file, "w") do io
sixel_encode(io, img, enc)
end

img_readback = open(tmp_file, "r") do io
sixel_decode(eltype(img), io, dec)
end

# 30 is actually pretty good given that sixel encode always do quantization
@test assess_psnr(img, img_readback) > 30
end
end
end
3 changes: 2 additions & 1 deletion test/runtests.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Sixel
using Test
using ImageCore, TestImages
using ImageCore, IndirectArrays, TestImages
using ImageQualityIndexes
using LinearAlgebra

sixel_output = Sixel.is_sixel_supported()
Expand Down

0 comments on commit 86fdae9

Please sign in to comment.