Skip to content

Commit

Permalink
Add support for parsing 8-digit and 4-digit hex notations
Browse files Browse the repository at this point in the history
  • Loading branch information
kimikage committed Dec 7, 2019
1 parent 1bc194e commit 38b4562
Show file tree
Hide file tree
Showing 2 changed files with 75 additions and 37 deletions.
104 changes: 70 additions & 34 deletions src/parse.jl
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,24 @@ chop1(x) = SubString(x, 1, lastindex(x) - 1) # `chop` is slightly slow

# Parse a number used in the "rgb()" or "hsl()" color.
function parse_rgb(num::AbstractString)
if num[end] == '%'
if @inbounds num[end] == '%'
return N0f8(clamp(parse(Int, chop1(num), base=10) / 100, 0, 1))
else
return reinterpret(N0f8, UInt8(clamp(parse(Int, num, base=10), 0, 255)))
v = clamp(parse(Int, num, base=10), 0, 255)
return reinterpret(N0f8, unsafe_trunc(UInt8, v))
end
end

function parse_hsl_hue(num::AbstractString)
if num[end] == '%'
if @inbounds num[end] == '%'
error("hue cannot end in %")
else
return parse(Int, num, base=10)
end
end

function parse_hsl_sl(num::AbstractString)
if num[end] != '%'
if @inbounds num[end] != '%'
error("saturation and lightness must end in %")
else
return parse(Int, chop1(num), base=10) / 100
Expand All @@ -40,7 +41,7 @@ end

# Parse a number used in the alpha field of "rgba()" and "hsla()".
function parse_alpha_num(num::AbstractString)
if num[end] == '%'
if @inbounds num[end] == '%'
return parse(Int, chop1(num), base=10) / 100f0
else
# `parse(Float32, num)` is somewhat slow on Windows(x86_64-w64-mingw32).
Expand All @@ -55,67 +56,85 @@ function parse_alpha_num(num::AbstractString)
end

function _parse_colorant(desc::AbstractString)
n0f8(x) = reinterpret(N0f8, unsafe_trunc(UInt8, x))
mat = match(col_pat_hex, desc)
if mat != nothing
prefix = mat.captures[1]
len = length(mat.captures[2])
digits = parse(UInt32, mat.captures[2], base=16)
@inbounds prefix, len = mat.captures[1], length(mat.captures[2])
@inbounds digits = parse(UInt32, mat.captures[2], base=16)
if len == 6
return convert(RGB{N0f8}, reinterpret(RGB24, digits))
elseif len == 3
return RGB{N0f8}(reinterpret(N0f8, UInt8(((digits&0xF00)>>8) * 17)),
reinterpret(N0f8, UInt8(((digits&0x0F0)>>4) * 17)),
reinterpret(N0f8, UInt8(((digits&0x00F)) * 17)))
elseif len == 8 || len == 4
error("8-digit and 4-digit hex notations are not supported yet.")
return RGB(n0f8((digits>>8) & 0xF * 0x11),
n0f8((digits>>4) & 0xF * 0x11),
n0f8((digits>>0) & 0xF * 0x11))
elseif len == 8
if @inbounds prefix[1] == '0'
return ARGB{N0f8}(n0f8(digits>>16),
n0f8(digits>> 8),
n0f8(digits>> 0),
n0f8(digits>>24))
else
return RGBA{N0f8}(n0f8(digits>>24),
n0f8(digits>>16),
n0f8(digits>> 8),
n0f8(digits>> 0))
end
elseif len == 4
if @inbounds prefix[1] == '0'
return ARGB{N0f8}(n0f8((digits>> 8) & 0xF * 0x11),
n0f8((digits>> 4) & 0xF * 0x11),
n0f8((digits>> 0) & 0xF * 0x11),
n0f8((digits>>12) & 0xF * 0x11))
else
return RGBA{N0f8}(n0f8((digits>>12) & 0xF * 0x11),
n0f8((digits>> 8) & 0xF * 0x11),
n0f8((digits>> 4) & 0xF * 0x11),
n0f8((digits>> 0) & 0xF * 0x11))
end
end
end
mat = match(col_pat_rgb, desc)
if mat != nothing
return RGB{N0f8}(parse_rgb(mat.captures[1]),
parse_rgb(mat.captures[2]),
parse_rgb(mat.captures[3]))
return @inbounds(RGB{N0f8}(parse_rgb(mat.captures[1]),
parse_rgb(mat.captures[2]),
parse_rgb(mat.captures[3])))
end

mat = match(col_pat_hsl, desc)
if mat != nothing
T = ColorTypes.eltype_default(HSL)
return HSL{T}(parse_hsl_hue(mat.captures[1]),
parse_hsl_sl(mat.captures[2]),
parse_hsl_sl(mat.captures[3]))
return @inbounds(HSL{T}(parse_hsl_hue(mat.captures[1]),
parse_hsl_sl(mat.captures[2]),
parse_hsl_sl(mat.captures[3])))
end

mat = match(col_pat_rgba, desc)
if mat != nothing
return RGBA{N0f8}(parse_rgb(mat.captures[1]),
parse_rgb(mat.captures[2]),
parse_rgb(mat.captures[3]),
parse_alpha_num(mat.captures[4]))
return @inbounds(RGBA{N0f8}(parse_rgb(mat.captures[1]),
parse_rgb(mat.captures[2]),
parse_rgb(mat.captures[3]),
parse_alpha_num(mat.captures[4])))
end

mat = match(col_pat_hsla, desc)
if mat != nothing
T = ColorTypes.eltype_default(HSLA)
return HSLA{T}(parse_hsl_hue(mat.captures[1]),
parse_hsl_sl(mat.captures[2]),
parse_hsl_sl(mat.captures[3]),
parse_alpha_num(mat.captures[4]))
return @inbounds(HSLA{T}(parse_hsl_hue(mat.captures[1]),
parse_hsl_sl(mat.captures[2]),
parse_hsl_sl(mat.captures[3]),
parse_alpha_num(mat.captures[4])))
end

sdesc = strip(desc)
c = get(color_names, sdesc, nothing)
if c != nothing
return RGB{N0f8}(reinterpret(N0f8, UInt8(c[1])),
reinterpret(N0f8, UInt8(c[2])),
reinterpret(N0f8, UInt8(c[3])))
return @inbounds RGB{N0f8}(n0f8(c[1]), n0f8(c[2]), n0f8(c[3]))
end
# since `lowercase` is slightly slow, it is applied only when needed
ldesc = lowercase(sdesc)
c = get(color_names, ldesc, nothing)
if c != nothing
return RGB{N0f8}(reinterpret(N0f8, UInt8(c[1])),
reinterpret(N0f8, UInt8(c[2])),
reinterpret(N0f8, UInt8(c[3])))
return @inbounds RGB{N0f8}(n0f8(c[1]), n0f8(c[2]), n0f8(c[3]))
end

if ldesc == "transparent"
Expand Down Expand Up @@ -146,7 +165,8 @@ slightly different than W3C named colors in some cases), `rgb()`, `hsl()`,
- `Colorant`: literal Colorant
- `desc`: color name or description
A literal Colorant will parse according to the `desc` string (usually returning an `RGB`); any more specific choice will return a color of the specified type.
A literal Colorant will parse according to the `desc` string (usually returning
an `RGB`); any more specific choice will return a color of the specified type.
# Returns
Expand All @@ -158,7 +178,23 @@ A literal Colorant will parse according to the `desc` string (usually returning
- an `HSLA` color if `hsla(h, s, l, a)` was used
- an `ARGB{N0f8}` color if `0xAARRGGBB`/`0xARGB` was used
- a specific `Colorant` type as specified in the first argument
!!! note "Note for hex notations"
You can parse not only the CSS-style hex notations `#RRGGBB`/`#RGB`, but
also `0xRRGGBB`/`0xRGB`.
You can also parse the 8-digit or 4-digit hex notation into an RGB color
with alpha. However, the result depends on the prefix (i.e. `#` or `0x`).
```@example
julia> parse(Colorant, "#FF8800AA") # transparent orange
RGBA{N0f8}(1.0,0.533,0.0,0.667)
julia> parse(Colorant, "0xFF8800AA") # opaque purple
ARGB{N0f8}(0.533,0.0,0.667,1.0)
```
"""
Base.parse(::Type{C}, desc::AbstractString) where {C<:Colorant} = _parse_colorant(C, supertype(C), desc)
Base.parse(::Type{C}, desc::Symbol) where {C<:Colorant} = parse(C, string(desc))
Expand Down
8 changes: 5 additions & 3 deletions test/parse.jl
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@ using FixedPointNumbers
@test parse(Colorant, "#D0FF58") === RGB(r8(0xD0),r8(0xFF),r8(0x58))
@test parse(Colorant, "0xd0ff58") === RGB(r8(0xD0),r8(0xFF),r8(0x58))
@test parse(Colorant, "#FB0") === RGB(r8(0xFF),r8(0xBB),r8(0x00))
@test_throws ErrorException parse(Colorant, "#FB0A")
@test parse(Colorant, "#FB0A") === RGBA(r8(0xFF),r8(0xBB),r8(0x00),r8(0xAA))
@test parse(Colorant, "0xFB0A") === ARGB(r8(0xBB),r8(0x00),r8(0xAA),r8(0xFF))
@test parse(Colorant, "#FFBB00AA") === RGBA(r8(0xFF),r8(0xBB),r8(0x00),r8(0xAA))
@test parse(Colorant, "0xFFBB00AA") === ARGB(r8(0xBB),r8(0x00),r8(0xAA),r8(0xFF))
@test_throws ErrorException parse(Colorant, "#BAD05")
@test_throws ErrorException parse(Colorant, "#BAD0007")
@test_throws ErrorException parse(Colorant, "#FFBB00AA") # not supported yet
@test_throws ErrorException parse(Colorant, "0xFFBB00AA") # not supported yet
@test_throws ErrorException parse(Colorant, "#BAD000009")

# rgb()
@test parse(Colorant, "rgb(55,217,127)") === RGB{N0f8}(r8(0x37),r8(0xd9),r8(0x7f))
Expand Down

0 comments on commit 38b4562

Please sign in to comment.