Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFC] Add support for parsing 8-digit and 4-digit hex notations #371

Merged
merged 1 commit into from
Jan 6, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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