I am trying to get the RGB values of a CSS color string and wonder how good my code is:
object Color {
def stringToInts(colorString: String): Option[(Int, Int, Int)] = {
val trimmedColorString: String = colorString.trim.replaceAll("#", "")
val longColorString: Option[String] = trimmedColorString.length match {
// allow only strings with either 3 or 6 letters
case 3 => Some(trimmedColorString.flatMap(character => s"$character$character"))
case 6 => Some(trimmedColorString)
case _ => None
}
val values: Option[Seq[Int]] = longColorString.map(_
.foldLeft(Seq[String]())((accu, character) => accu.lastOption.map(_.toSeq) match {
case Some(Seq(_, _)) => accu :+ s"$character" // previous value is complete => start with succeeding
case Some(Seq(c)) => accu.dropRight(1) :+ s"$c$character" // complete the previous value
case _ => Seq(s"$character") // start with an incomplete first value
})
.flatMap(hexString => scala.util.Try(Integer.parseInt(hexString, 16)).toOption)
// .flatMap(hexString => try {
// Some(Integer.parseInt(hexString, 16))
// } catch {
// case _: Exception => None
// })
)
values.flatMap(values => values.size match {
case 3 => Some((values.head, values(1), values(2)))
case _ => None
})
}
}
// example:
println(Color.stringToInts("#abc")) // prints Some((170,187,204))
You may run that example on https://scastie.scala-lang.org
The parts of that code I am most unsure about are
match
in the foldLeft
(is it a good idea to use string interpolation or can the code be written shorter without string interpolation?)Integer.parseInt
in conjunction with try
(can I use a prettier alternative in Scala?)But I expect most parts of my code to be improvable. I do not want to introduce new libraries in addition to com.itextpdf
to shorten my code, but using com.itextpdf
functions is an option. (The result of stringToInts
is going to be converted into a new com.itextpdf.kernel.colors.DeviceRgb(...)
, thus I have installed com.itextpdf
anyway.)
Tests defining the expected function:
import org.scalatest.{BeforeAndAfterEach, FunSuite}
class ColorTest extends FunSuite with BeforeAndAfterEach {
test("shorthand mixed case color") {
val actual: Option[(Int, Int, Int)] = Color.stringToInts("#Fa#F")
val expected = (255, 170, 255)
assert(actual === Some(expected))
}
test("mixed case color") {
val actual: Option[(Int, Int, Int)] = Color.stringToInts("#1D9a06")
val expected = (29, 154, 6)
assert(actual === Some(expected))
}
test("too short long color") {
val actual: Option[(Int, Int, Int)] = Color.stringToInts("#1D9a6")
assert(actual === None)
}
test("too long shorthand color") {
val actual: Option[(Int, Int, Int)] = Color.stringToInts("#1D9a")
assert(actual === None)
}
test("invalid color") {
val actual: Option[(Int, Int, Int)] = Color.stringToInts("#1D9g06")
assert(actual === None)
}
}
At the moment of writing this answer the other answers don't properly handle rgb()
, rgba()
and named colors cases. Color strings that start with hashes (#
) are only a part of the deal.
As you have iText7
as a dependency and iText7
has a pdfHTML
add-on which means the logic for parsing CSS
colors obviously must be somewhere in iText7
and, more importantly, it must handle various range of CSS color cases. The question is only about finding the right place. Fortunately, this API is public and easy to use.
The method you are interested in is WebColors.getRGBAColor()
from package com.itextpdf.kernel.colors
which accepts a CSS
color string a returns a 4-element array with R
, G
, B
, A
values (last one stands for alpha, i.e. transparency).
You can use those values to create a color right away (code in Java):
float[] rgbaColor = WebColors.getRGBAColor("#ababab");
Color color = new DeviceRgb(rgbaColor[0], rgbaColor[1], rgbaColor[2]);
In Scala it must be something like
val rgbaColor = WebColors.getRGBAColor("#ababab");
val color = new DeviceRgb(rgbaColor(0), rgbaColor(1), rgbaColor(2));