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 31, 2019
1 parent 0890b41 commit a758895
Show file tree
Hide file tree
Showing 2 changed files with 70 additions and 39 deletions.
101 changes: 65 additions & 36 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,46 +56,68 @@ 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])
if mat !== nothing
prefix, len = mat.captures[1], length(mat.captures[2])
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 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 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
if mat !== nothing
return 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
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]))
end

mat = match(col_pat_rgba, desc)
if mat != nothing
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]))
end

mat = match(col_pat_hsla, desc)
if mat != nothing
if mat !== nothing
T = ColorTypes.eltype_default(HSLA)
return HSLA{T}(parse_hsl_hue(mat.captures[1]),
parse_hsl_sl(mat.captures[2]),
Expand All @@ -104,36 +127,25 @@ function _parse_colorant(desc::AbstractString)

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])))
end
c !== nothing && return RGB{N0f8}(n0f8(c[1]), n0f8(c[2]), n0f8(c[3]))

# 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])))
end
c !== nothing && return RGB{N0f8}(n0f8(c[1]), n0f8(c[2]), n0f8(c[3]))

if ldesc == "transparent"
return RGBA{N0f8}(0,0,0,0)
end
ldesc == "transparent" && return RGBA{N0f8}(0,0,0,0)

wo_spaces = replace(ldesc, r"(?<=[^ ]{3}) (?=[^ ]{3})" => "")
c = get(color_names, wo_spaces, nothing)
if c != nothing
if c !== nothing
camel = replace(titlecase(ldesc), " " => "")
Base.depwarn(
"""
The X11 color names with spaces are not recommended because they are not allowed in the SVG/CSS.
Use "$camel" or "$wo_spaces" instead.
""", :parse)
return RGB{N0f8}(reinterpret(N0f8, UInt8(c[1])),
reinterpret(N0f8, UInt8(c[2])),
reinterpret(N0f8, UInt8(c[3])))
return RGB{N0f8}(n0f8(c[1]), n0f8(c[2]), n0f8(c[3]))
end

error("Unknown color: ", desc)
Expand All @@ -160,7 +172,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 @@ -172,11 +185,27 @@ 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 "Note for X11 named colors"
The X11 color names with spaces (e.g. "sea green") are not recommended
because they are not allowed in the SVG/CSS.
!!! 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 @@ -21,11 +21,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 a758895

Please sign in to comment.