Created attachment 2369 [details]
patch to fix reported bug
I attempted to post this to r-devel to confirm it is a bug prior to posting here, but my plain-text e-mail sending capabilities are not good and what I sent got mangled by mailman, so I repost here as I'm reasonably confident this is a bug.
It appears that the chromatic adaptation feature of `grDevices::convertColor`is broken, and likely has been for many years. While a little surprising, it is an obscure enough feature that there is some possibility this is actually broken, as opposed to user error on my part.
rgb.in <- c("#CCCCCC", "#EEEEEE")
clr <- t(col2rgb(rgb.in)) / 255
clr.out <- convertColor(clr, "sRGB", "sRGB")
##  "#CCCCCC" "#EEEEEE"
Instead, if we try to change the whitepoint with chromatic adaptation:
convertColor(clr, "sRGB", "sRGB", "D65", "D50")
## Error in match.arg(from, nWhite) :
## 'arg' must be NULL or a character vector
This appears to be because `grDevices:::chromaticAdaptation` expects the whitepoints to be provided in the character format (e.g. "D65"), but they are already converted by `convertColor`. After applying the attached patch, we get:
clr.out <- convertColor(clr, "sRGB", "sRGB", "D65", "D50")
##  "#DACAB0" "#FEECCE"
I do not have a great way of confirming that the conversion is correct with my changes, but I did verify that the `M` matrix computed within`grDevics:::chromaticAdaptation` for the "D65"->"D50" conversion (approximately) matches the corresponding matrix from brucelindbloom.com chromatic adaptation page:
Additionally visual inspection via:
is consistent with a shift from bluer indirect daylight ("D65") to yellower direct daylight ("D50") illuminant.
It is worth noting that the adaption method currently in`grDevices:::chromaticAdaptation` appears to be the "Von Kries" method, not the "Bradford" method as documented in `?convertColor` and in the comments of the sources. I base this on comparing the cone response domain matrices on the aforementioned brucelindbloom.com page to the `Ma` matrix defined in`grDevics:::chromaticAdaptation`. If I am correct about this `?convertColor` should also be updated, or the adaptation method should be changed to "Bradford".
The attached patch is intended to minimize the required changes, but as a result it does a bit more computation than ideal (e.g. double transposition). However, the additional computation time is small compared to the overall execution time of the function (aside: I am working on a patch to improve the performance of this function).
R version 3.5.1 (2018-07-02)
Platform: x86_64-apple-darwin15.6.0 (64-bit)
Running under: macOS High Sierra 10.13.6
Matrix products: default
attached base packages:
 stats graphics grDevices utils datasets methods base
loaded via a namespace (and not attached):
Created attachment 2370 [details]
Example of chromatic adaptation effect (image)
Upon further examination of the sources it is now apparent to me that `convertColor` should enforce that `from.white.ref` and `to.white.ref` are length one, otherwise code like `c2to3(white.points[, from.ref.white])` will produce non-sensical results because it implicitly assumes its input to be length 2:
c2to3 <- function(col) c(col[1L]/col[2L], 1, (1 - sum(col))/col[2L])
The problem is `sum(col)`. Two options come to mind:
1. Use `sum(col[, 1])` to silently ignore additional white points
2. Stop if `from.white.ref` and `to.white.ref` are longer than one
Incidentally, this also affects `make.rgb`.
I have committed (r75340) a variation on your patch/suggestions ...
1. Changed comment to refer to Von Kries scaling algorithm
2. Removed the c2to3() calls within chromaticAdaptation() (because they have already happened outside chromaticAdaptation() as you pointed out)
3. Changed references to 'from$white' to 'from$reference.white' within convertColor() (because the former is NULL), to enforce the intended semantics so that your example ...
convertColor(clr, "sRGB", "sRGB", "D65", "D50")
... is actually illegal (because sRGB has an implicit white point), but something like this actually works ...
xyzD65 <- convertColor(clr, "sRGB", "XYZ")
xyzD50 <- convertColor(xyzD65, "XYZ", "XYZ", "D65", "D50")
4. Added the double-transposition in the call to chromaticAdaptation() within convertColor()
5. Changed c2to3() to use ...
... to only use first two values. There could be a whole lot more input checking added to these functions, but there are more thorough implementations of this sort of thing in packages 'colorspace' and 'colorscience' (at least), so I do not think these 'grDevices' functions are receiving heavy enough use to warrant a lot of extra work at this stage.
Confirmation that these changes produce sensible results for you would be welcome.
I will review and run tests today.
I ran some tests and the function appears to behave as expected, although there
is one change in behavior I note below in 4.
For reference the tests I ran are on github:
Additional comments (the attached patch helps to illustrate them):
1. Documentation should also be updated to reflect the correct chromatic
2. Good catch on your #3 `$white` vs `$reference.white`; I missed that.
Unfortunately this means I missed other related problems (see below).
3. I believe the likely reason for the above bug is the inconsistency between
`make.rgb` (@63) and `colorConverter` (@96). Both create `colorConverter` S3
objects, but the former does it with a `reference.white` element and the
latter with a `white` element. If `make.rgb` used `colorConverter` to
instantiate its `colorConverter` object this inconsistency would have been
avoided. As it stands the inconsistency remains and anyone using
`colorConverter` directly with set reference whites will not get the expected
4. The bug fixed by #3 unfortunately caused incorrect conversions for color
spaces with a non-"D65" white point. Effectively, all RGB color spaces were
implicitly assumed to be "D65". Fortunately this only affects the "CIE RGB"
color space, which I'm guessing is not broadly used. It does mean that
behavior of the function will change for "CIE RGB" and any other `make.rgb`
user created color spaces with "non-D65" reference white points.
Created attachment 2371 [details]
patch that illustrates latest round of comments