;;; "colorspc.scm" color-space conversions
;Copyright 2001, 2002 Aubrey Jaffer
;
;Permission to copy this software, to modify it, to redistribute it,
;to distribute modified versions, and to use it for any purpose is
;granted, subject to the following restrictions and understandings.
;
;1.  Any copy made of this software must include this copyright notice
;in full.
;
;2.  I have made no warranty or representation that the operation of
;this software will be error-free, and I am under no obligation to
;provide any services, by way of maintenance, update, or otherwise.
;
;3.  In conjunction with products arising from the use of this
;material, there shall be no use of my name in any advertising,
;promotional, or sales literature without prior written consent in
;each case.

(require 'logical)
(require 'multiarg/and-)
(require-if 'compiling 'sort)
(require-if 'compiling 'ciexyz)
;@
(define (color:linear-transform matrix row)
  (map (lambda (mrow) (apply + (map * mrow row)))
       matrix))

(define RGB709:into-matrix
  '((  3.240479 -1.537150 -0.498535 )
    ( -0.969256  1.875992  0.041556 )
    (  0.055648 -0.204043  1.057311 )))

;;; http://www.pima.net/standards/it10/PIMA7667/PIMA7667-2001.PDF gives
;;; matrix identical to sRGB:from-matrix, but colors drift under
;;; repeated conversions to and from CIEXYZ.  Instead use RGB709.

(define RGB709:from-matrix
  '((  0.412453  0.357580  0.180423 )
    (  0.212671  0.715160  0.072169 )
    (  0.019334  0.119193  0.950227 )))

;; From http://www.cs.rit.edu/~ncs/color/t_convert.html
;@
(define (CIEXYZ->RGB709 XYZ)
  (color:linear-transform RGB709:into-matrix XYZ))
(define (RGB709->CIEXYZ rgb)
  (color:linear-transform RGB709:from-matrix rgb))

;;; From http://www.w3.org/Graphics/Color/sRGB.html

(define sRGB-log
  (lambda (sv)
    (if (<= sv 0.00304)
	(* 12.92 sv)
	(+ -0.055 (* 1.055 (expt sv 10/24))))))
(define sRGB-exp
  (lambda (x)
    (if (<= x 0.03928)
	(/ x 12.92)
	(expt (/ (+ 0.055 x) 1.055) 2.4))))

;; Clipping as recommended by sRGB spec.
;@
(define (CIEXYZ->sRGB XYZ)
  (map (lambda (sv)
	 (inexact->exact (round (* 255 (sRGB-log (max 0 (min 1 sv)))))))
       (color:linear-transform RGB709:into-matrix XYZ)))
(define (sRGB->CIEXYZ sRGB)
  (color:linear-transform
   RGB709:from-matrix
   (map sRGB-exp
	(map (lambda (b8v) (/ b8v 255.0)) sRGB))))

;;; sRGB values are sometimes written as 24-bit integers 0xRRGGBB
;@
(define (xRGB->sRGB xRGB)
  (list (ash xRGB -16)
	(logand (ash xRGB -8) 255)
	(logand xRGB 255)))
(define (sRGB->xRGB sRGB)
  (apply + (map * sRGB '(#x10000 #x100 #x1))))
;@
(define (xRGB->CIEXYZ xRGB) (sRGB->CIEXYZ (xRGB->sRGB xRGB)))
(define (CIEXYZ->xRGB xyz)   (sRGB->xRGB (CIEXYZ->sRGB xyz)))

;;;  http://www.pima.net/standards/it10/PIMA7667/PIMA7667-2001.PDF
;;;	    Photography - Electronic still picture imaging -
;;;		 Extended sRGB color encoding - e-sRGB

(define e-sRGB-log
  (lambda (sv)
    (cond ((< sv -0.0031308)
	   (- 0.055 (* 1.055 (expt (- sv) 10/24))))
	  ((<= sv 0.0031308)
	   (* 12.92 sv))
	  (else (+ -0.055 (* 1.055 (expt sv 10/24)))))))
(define e-sRGB-exp
  (lambda (x)
    (cond ((< x -0.04045)
	   (- (expt (/ (- 0.055 x) 1.055) 2.4)))
	  ((<= x 0.04045)
	   (/ x 12.92))
	  (else (expt (/ (+ 0.055 x) 1.055) 2.4)))))
;@
(define (CIEXYZ->e-sRGB n XYZ)
  (define two^n-9 (ash 1 (- n 9)))
  (define offset (* 3 (ash 1 (- n 3))))
  (map (lambda (x)
	 (+ (inexact->exact (round (* x 255 two^n-9))) offset))
       (map e-sRGB-log
	    (color:linear-transform
	     RGB709:into-matrix
	     XYZ))))
;@
(define (e-sRGB->CIEXYZ n rgb)
  (define two^n-9 (ash 1 (- n 9)))
  (define offset (* 3 (ash 1 (- n 3))))
  (color:linear-transform
   RGB709:from-matrix
   (map e-sRGB-exp
	(map (lambda (b8v) (/ (- b8v offset) 255.0 two^n-9))
	     rgb))))
;@
(define (sRGB->e-sRGB n sRGB)
  (define two^n-9 (ash 1 (- n 9)))
  (define offset (* 3 (ash 1 (- n 3))))
  (map (lambda (x) (+ offset (* two^n-9 x))) sRGB))
;@
(define (e-sRGB->sRGB n rgb)
  (define two^n-9 (ash 1 (- n 9)))
  (define offset (* 3 (ash 1 (- n 3))))
  (map (lambda (x) (/ (- x offset) two^n-9)) rgb))
;@
(define (e-sRGB->e-sRGB n rgb m)
  (define shft (- m n))
  (cond ((zero? shft) rgb)
	(else (map (lambda (x) (ash x shft)) rgb))))

;;; From http://www.cs.rit.edu/~ncs/color/t_convert.html

;;; CIE 1976 L*a*b* is based directly on CIE XYZ and is an attampt to
;;; linearize the perceptibility of color differences. The non-linear
;;; relations for L*, a*, and b* are intended to mimic the logarithmic
;;; response of the eye. Coloring information is referred to the color
;;; of the white point of the system, subscript n.

;;;; L* is CIE lightness
;;;     L* = 116 * (Y/Yn)^1/3 - 16    for Y/Yn > 0.008856
;;;     L* = 903.3 * Y/Yn             otherwise

(define (CIE:Y/Yn->L* Y/Yn)
  (if (> Y/Yn 0.008856)
      (+ -16 (* 116 (expt Y/Yn 1/3)))
      (* 903.3 Y/Yn)))
(define (CIE:L*->Y/Yn L*)
  (cond ((<= L* (* 903.3 0.008856))
	 (/ L* 903.3))
	((<= L* 100.)
	 (expt (/ (+ L* 16) 116) 3))
	(else 1)))

;;; a* = 500 * ( f(X/Xn) - f(Y/Yn) )
;;; b* = 200 * ( f(Y/Yn) - f(Z/Zn) )
;;;     where f(t) = t^1/3              for t > 0.008856
;;;           f(t) = 7.787 * t + 16/116 otherwise

(define (ab-log t)
  (if (> t 0.008856)
      (expt t 1/3)
      (+ 16/116 (* t 7.787))))
(define (ab-exp f)
  (define f3 (expt f 3))
  (if (> f3 0.008856)
      f3
      (/ (- f 16/116) 7.787)))
;@
(define (CIEXYZ->L*a*b* XYZ . white-point)
  (apply (lambda (X/Xn Y/Yn Z/Zn)
	   (list (CIE:Y/Yn->L* Y/Yn)
		 (* 500 (- (ab-log X/Xn) (ab-log Y/Yn)))
		 (* 200 (- (ab-log Y/Yn) (ab-log Z/Zn)))))
	 (map / XYZ (if (null? white-point)
			CIEXYZ:D65
			(car white-point)))))

;;; Here Xn, Yn and Zn are the tristimulus values of the reference white.
;@
(define (L*a*b*->CIEXYZ L*a*b* . white-point)
  (apply (lambda (Xn Yn Zn)
	   (apply (lambda (L* a* b*)
		    (let* ((Y/Yn (CIE:L*->Y/Yn L*))
			   (fY/Yn (ab-log Y/Yn)))
		      (list (* Xn (ab-exp (+ fY/Yn (/ a* 500))))
			    (* Yn Y/Yn)
			    (* Zn (ab-exp (+ fY/Yn (/ b* -200)))))))
		  L*a*b*))
	 (if (null? white-point)
	     CIEXYZ:D65
	     (car white-point))))

;;; XYZ to CIELUV

;;; CIE 1976 L*u*u* (CIELUV) is based directly on CIE XYZ and is another
;;; attampt to linearize the perceptibility of color differences.  L* is
;;; CIE lightness as for L*a*b* above.  The non-linear relations for u*
;;; and v* are:

;;;     u* =  13 L* ( u' - un' )
;;;     v* =  13 L* ( v' - vn' )

;;; The quantities un' and vn' refer to the reference white or the light
;;; source; for the 2.o observer and illuminant C, un' = 0.2009, vn' =
;;; 0.4610. Equations for u' and v' are given below:

;;;     u' = 4 X / (X + 15 Y + 3 Z)
;;;     v' = 9 Y / (X + 15 Y + 3 Z)

(define (XYZ->uv XYZ)
  (apply (lambda (X Y Z)
	   (define denom (+ X (* 15 Y) (* 3 Z)))
	   (if (zero? denom)
	       '(4. 9.)
	       (list (/ (* 4 X) denom)
		     (/ (* 9 Y) denom))))
	 XYZ))
;@
(define (CIEXYZ->L*u*v* XYZ . white-point)
  (set! white-point (if (null? white-point)
			CIEXYZ:D65
			(car white-point)))
  (let* ((Y/Yn (/ (cadr XYZ) (cadr white-point)))
	 (L* (CIE:Y/Yn->L* Y/Yn)))
    (cons L* (map (lambda (q) (* 13 L* q))
		  (map - (XYZ->uv XYZ) (XYZ->uv white-point))))))

;;; CIELUV to XYZ

;;; The transformation from CIELUV to XYZ is performed as following:

;;;     u' = u / ( 13 L* ) + un
;;;     v' = v / ( 13 L* ) + vn
;;;     X = 9 Y u' / 4 v'
;;;     Z = ( 12 Y - 3 Y u' - 20 Y v' ) / 4 v'
;@
(define (L*u*v*->CIEXYZ L*u*v* . white-point)
  (set! white-point (if (null? white-point)
			CIEXYZ:D65
			(car white-point)))
  (apply (lambda (un vn)
	   (apply (lambda (L* u* v*)
		    (if (not (positive? L*))
			'(0. 0. 0.)
			(let* ((up (+ (/ u* 13 L*) un))
			       (vp (+ (/ v* 13 L*) vn))
			       (Y (* (CIE:L*->Y/Yn L*) (cadr white-point))))
			  (list (/ (* 9 Y up) 4 vp)
				Y
				(/ (* Y (+ 12 (* -3 up) (* -20 vp))) 4 vp)))))
		  L*u*v*))
	 (XYZ->uv white-point)))

;;; http://www.inforamp.net/~poynton/PDFs/coloureq.pdf

(define pi (* 4 (atan 1)))
(define pi/180 (/ pi 180))
;@
(define (L*a*b*->L*C*h lab)
  (define h (/ (atan (caddr lab) (cadr lab)) pi/180))
  (list (car lab)
	(sqrt (apply + (map * (cdr lab) (cdr lab))))
	(if (negative? h) (+ 360 h) h)))
;@
(define (L*C*h->L*a*b* lch)
  (apply (lambda (L* C* h)
	   (set! h (* h pi/180))
	   (list L*
		 (* C* (cos h))
		 (* C* (sin h))))
	 lch))
;@
(define (L*a*b*:DE* lab1 lab2)
  (sqrt (apply + (map (lambda (x) (* x x)) (map - lab1 lab2)))))

;;; http://www.colorpro.com/info/data/cie94.html

(define (color:process-params parametric-factors)
  (define ans
    (case (length parametric-factors)
      ((0) #f)
      ((1) (if (list? parametric-factors)
	       (apply color:process-params parametric-factors)
	       (append parametric-factors '(1 1))))
      ((2) (append parametric-factors '(1)))
      ((3) parametric-factors)
      (else (slib:error 'parametric-factors 'too-many parametric-factors))))
  (and ans
       (for-each (lambda (obj)
		   (if (not (number? obj))
		       (slib:error 'parametric-factors 'not 'number? obj)))
		 ans))
  ans)
;@
(define (L*C*h:DE*94 lch1 lch2 . parametric-factors)
  (define C* (sqrt (* (cadr lch1) (cadr lch2)))) ;Geometric mean
  (sqrt (apply + (map /
		      (map (lambda (x) (* x x)) (map - lch1 lch2))
		      (list 1		; S_l
			    (+ 1 (* .045 C*)) ; S_c
			    (+ 1 (* .015 C*))) ; S_h
		      (or (color:process-params parametric-factors)
			  '(1 1 1))))))

;;; CMC-DE is designed only for small color-differences.  But try to do
;;; something reasonable for large differences.  Use bisector (h*) of
;;; the hue angles if separated by less than 90.o; otherwise, pick h of
;;; the color with larger C*.
;@
(define (CMC-DE lch1 lch2 . parametric-factors)
  (apply (lambda (L* C* h_)		;Geometric means
	   (let ((ang1 (* pi/180 (caddr lch1)))
		 (ang2 (* pi/180 (caddr lch2))))
	     (cond ((>= 90 (abs (/ (atan (sin (- ang1 ang2))
					 (cos (- ang1 ang2)))
				   pi/180)))
		    (set! h_ (/ (atan (+ (sin ang1) (sin ang2))
				      (+ (cos ang1) (cos ang2)))
				pi/180)))
		   ((>= (cadr lch1) (cadr lch2)) (caddr lch1))
		   (else (caddr lch2))))
	   (let* ((C*^4 (expt C* 4))
		  (f    (sqrt (/ C*^4 (+ C*^4 1900))))
		  (T    (if (and (> h_ 164) (< h_ 345))
			    (+ 0.56 (abs (* 0.2 (cos (* (+ h_ 168) pi/180)))))
			    (+ 0.36 (abs (* 0.4 (cos (* (+ h_ 35) pi/180)))))))
		  (S_l  (if (< L* 16)
			    0.511
			    (/ (* 0.040975 L*) (+ 1 (* 0.01765 L*)))))
		  (S_c  (+ (/ (* 0.0638 C*) (+ 1 (* 0.0131 C*))) 0.638))
		  (S_h  (* S_c (+ (* (+ -1 T) f) 1))))
	     (sqrt (apply
		    + (map /
			   (map (lambda (x) (* x x)) (map - lch1 lch2))
			   (list S_l S_c S_h)
			   (or (color:process-params parametric-factors)
			       '(2 1 1)))))))
	 (map sqrt (map * lch1 lch2))))

;;; Chromaticity
;@
(define (XYZ->chromaticity XYZ)
  (define sum (apply + XYZ))
  (list (/ (car XYZ) sum) (/ (cadr XYZ) sum)))
;@
(define (chromaticity->CIEXYZ x y)
  (list x y (- 1 x y)))
(define (chromaticity->whitepoint x y)
  (list (/ x y) 1 (/ (- 1 x y) y)))
;@
(define (XYZ->xyY XYZ)
  (define sum (apply + XYZ))
  (if (zero? sum)
      '(0 0 0)
      (list (/ (car XYZ) sum) (/ (cadr XYZ) sum) (cadr XYZ))))
;@
(define (xyY->XYZ xyY)
  (define x (car xyY))
  (define y (cadr xyY))
  (if (zero? y)
      '(0 0 0)
      (let ((Y/y (/ (caddr xyY) y)))
	(list (* Y/y x) (caddr xyY) (* Y/y (- 1 x y))))))
;@
(define (xyY:normalize-colors lst . n)
  (define (nthcdr n lst) (if (zero? n) lst (nthcdr (+ -1 n) (cdr lst))))
  (define Ys (map caddr lst))
  (set! n (if (null? n) 1 (car n)))
  (let ((max-Y (if (positive? n)
		   (* n (apply max Ys))
		   (let ()
		     (require 'sort)
		     (apply max (nthcdr (- n) (sort Ys >=)))))))
    (map (lambda (xyY)
	   (let ((x (max 0 (car xyY)))
		 (y (max 0 (cadr xyY))))
	     (define sum (max 1 (+ x y)))
	     (list (/ x sum)
		   (/ y sum)
		   (max 0 (min 1 (/ (caddr xyY) max-Y))))))
	 lst)))

;;;  http://www.aim-dtp.net/aim/technology/cie_xyz/cie_xyz.htm:
;;;  Illuminant D65                           0.312713 0.329016
;; (define CIEXYZ:D65 (chromaticity->whitepoint 0.312713 0.329016))
;; (define CIEXYZ:D65 (chromaticity->whitepoint 0.3127 0.3290))
;@
(define CIEXYZ:D50 (chromaticity->whitepoint 0.3457 0.3585))

;;; With its 16-bit resolution, e-sRGB-16 is extremely sensitive to
;;; whitepoint.  Even the 6 digits of precision specified above is
;;; insufficient to make (color->e-srgb 16 d65) ==> (57216 57216 57216)
;@
(define CIEXYZ:D65 (e-sRGB->CIEXYZ 16 '(57216 57216 57216)))

;;; http://www.efg2.com/Lab/Graphics/Colors/Chromaticity.htm CIE 1931:
;@
(define CIEXYZ:A (chromaticity->whitepoint 0.44757 0.40745)) ; 2856.K
(define CIEXYZ:B (chromaticity->whitepoint 0.34842 0.35161)) ; 4874.K
(define CIEXYZ:C (chromaticity->whitepoint 0.31006 0.31616)) ; 6774.K
(define CIEXYZ:E (chromaticity->whitepoint 1/3 1/3)) ; 5400.K

;;; Converting spectra
(define cie:x-bar #f)
(define cie:y-bar #f)
(define cie:z-bar #f)
;@
(define (load-ciexyz . path)
  (let ((path (if (null? path)
		  (in-vicinity (library-vicinity) "cie1931.xyz")
		  (car path))))
    (set! cie:x-bar (make-vector 80))
    (set! cie:y-bar (make-vector 80))
    (set! cie:z-bar (make-vector 80))
    (call-with-input-file path
      (lambda (iprt)
	(do ((wlen 380 (+ 5 wlen))
	     (idx 0 (+ 1 idx)))
	    ((>= wlen 780))
	  (let ((rlen (read iprt)))
	    (if (not (eqv? wlen rlen))
		(slib:error path 'expected wlen 'not rlen))
	    (vector-set! cie:x-bar idx (read iprt))
	    (vector-set! cie:y-bar idx (read iprt))
	    (vector-set! cie:z-bar idx (read iprt))))))))
;@
(define (read-cie-illuminant path)
  (define siv (make-vector 107))
  (call-with-input-file path
    (lambda (iprt)
      (do ((idx 0 (+ 1 idx)))
	  ((>= idx 107) siv)
	(vector-set! siv idx (read iprt))))))
;@
(define (read-normalized-illuminant path)
  (define siv (read-cie-illuminant path))
  (let ((yw (/ (cadr (spectrum->XYZ siv 300e-9 830e-9)))))
    (illuminant-map (lambda (w x) (* x yw)) siv)))
;@
(define (illuminant-map proc siv)
  (define prod (make-vector 107))
  (do ((idx 106 (+ -1 idx))
       (w 830e-9 (+ -5e-9 w)))
      ((negative? idx) prod)
    (vector-set! prod idx (proc w (vector-ref siv idx)))))
;@
(define (illuminant-map->XYZ proc siv)
  (spectrum->XYZ (illuminant-map proc siv) 300e-9 830e-9))
;@
(define (wavelength->XYZ wl)
  (if (not cie:y-bar) (require 'ciexyz))
  (set! wl (- (/ wl 5.e-9) 380/5))
  (if (<= 0 wl (+ -1 400/5))
      (let* ((wlf (inexact->exact (floor wl)))
	     (res (- wl wlf)))
	(define (interpolate vect idx res)
	  (+ (* res (vector-ref vect idx))
	     (* (- 1 res) (vector-ref vect (+ 1 idx)))))
	(list (interpolate cie:x-bar wlf res)
	      (interpolate cie:y-bar wlf res)
	      (interpolate cie:z-bar wlf res)))
      (slib:error 'wavelength->XYZ 'out-of-range wl)))
(define (wavelength->chromaticity wl)
  (XYZ->chromaticity (wavelength->XYZ wl)))
;@
(define (spectrum->XYZ . args)
  (define x 0)
  (define y 0)
  (define z 0)
  (if (not cie:y-bar) (require 'ciexyz))
  (case (length args)
    ((1)
     (set! args (car args))
     (do ((wvln 380.e-9 (+ 5.e-9 wvln))
	  (idx 0 (+ 1 idx)))
	 ((>= idx 80) (map (lambda (x) (/ x 80)) (list x y z)))
       (let ((inten (args wvln)))
	 (set! x (+ x (* (vector-ref cie:x-bar idx) inten)))
	 (set! y (+ y (* (vector-ref cie:y-bar idx) inten)))
	 (set! z (+ z (* (vector-ref cie:z-bar idx) inten))))))
    ((3)
     (let* ((vect (if (list? (car args)) (list->vector (car args)) (car args)))
	    (vlen (vector-length vect))
	    (x1 (cadr args))
	    (x2 (caddr args))
	    (xinc (/ (- x2 x1) (+ -1 vlen)))
	    (x->j (lambda (x) (inexact->exact (round (/ (- x x1) xinc)))))
	    (x->k (lambda (x) (inexact->exact (round (/ (- x 380.e-9) 5.e-9)))))
	    (j->x (lambda (j) (+ x1 (* j xinc))))
	    (k->x (lambda (k) (+ 380.e-9 (* k 5.e-9))))
	    (xlo (max (min x1 x2) 380.e-9))
	    (xhi (min (max x1 x2) 780.e-9))
	    (jhi (x->j xhi))
	    (khi (x->k xhi))
	    (jinc (if (negative? xinc) -1 1)))
       (if (<= (abs xinc) 5.e-9)
	   (do ((wvln (j->x (x->j xlo)) (+ wvln (abs xinc)))
		(jdx (x->j xlo) (+ jdx jinc)))
	       ((>= jdx jhi)
		(let ((nsmps (abs (- jhi (x->j xlo)))))
		  (map (lambda (x) (/ x nsmps)) (list x y z))))
	     (let ((ciedex (min 79 (x->k wvln)))
		   (inten (vector-ref vect jdx)))
	       (set! x (+ x (* (vector-ref cie:x-bar ciedex) inten)))
	       (set! y (+ y (* (vector-ref cie:y-bar ciedex) inten)))
	       (set! z (+ z (* (vector-ref cie:z-bar ciedex) inten)))))
	   (do ((wvln (k->x (x->k xlo)) (+ wvln 5.e-9))
		(kdx (x->k xlo) (+ kdx 1)))
	       ((>= kdx khi)
		(let ((nsmps (abs (- khi (x->k xlo)))))
		  (map (lambda (x) (/ x nsmps)) (list x y z))))
	     (let ((inten (vector-ref vect (x->j wvln))))
	       (set! x (+ x (* (vector-ref cie:x-bar kdx) inten)))
	       (set! y (+ y (* (vector-ref cie:y-bar kdx) inten)))
	       (set! z (+ z (* (vector-ref cie:z-bar kdx) inten))))))))
    (else (slib:error 'spectrum->XYZ 'wna args))))
(define (spectrum->chromaticity . args)
  (XYZ->chromaticity (apply spectrum->XYZ args)))
;@
(define blackbody-spectrum
  (let* ((c 2.998e8)
	 (h 6.626e-34)
	 (h*c (* h c))
	 (k 1.381e-23)
	 (pi*2*h*c*c (* 2 pi h*c c)))
    (lambda (temp . span)
      (define h*c/kT (/ h*c k temp))
      (define pi*2*h*c*c*span
	(* pi*2*h*c*c (if (null? span) 1.e-9 (car span))))
      (lambda (x)
	(/ pi*2*h*c*c*span
	   (expt x 5)
	   (- (exp (/ h*c/kT x)) 1))))))
;@
(define (temperature->XYZ temp . span)
  (spectrum->XYZ (apply blackbody-spectrum temp span)))	;was .5e-9
(define (temperature->chromaticity temp)
  (XYZ->chromaticity (temperature->XYZ temp)))
