// This file is based on the Go implementation found here:
// https://cs.opensource.google/go/go/+/refs/tags/go1.23.3:src/image/color/ycbcr.go
// which has the copyright notice:
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
///|
/// rgb_to_y_cb_cr converts an RGB triple to a Y'CbCr triple.
pub fn rgb_to_y_cb_cr(r : Byte, g : Byte, b : Byte) -> (Byte, Byte, Byte) {
// The JFIF specification says:
// Y' = 0.2990*R + 0.5870*G + 0.1140*B
// Cb = -0.1687*R - 0.3313*G + 0.5000*B + 128
// Cr = 0.5000*R - 0.4187*G - 0.0813*B + 128
// https://www.w3.org/Graphics/JPEG/jfif3.pdf says Y but means Y'.
let r1 = r.to_int()
let g1 = g.to_int()
let b1 = b.to_int()
// yy is in range [0,0xff].
//
// Note that 19595 + 38470 + 7471 equals 65536.
let yy = (19595 * r1 + 38470 * g1 + 7471 * b1 + (1 << 15)) >> 16
// The bit twiddling below is equivalent to
//
// cb := (-11056*r1 - 21712*g1 + 32768*b1 + 257<<15) >> 16
// if cb < 0 {
// cb = 0
// } else if cb > 0xff {
// cb = ^int32(0)
// }
//
// but uses fewer branches and is faster.
// Note that the uint8 type conversion in the return
// statement will convert ^int32(0) to 0xff.
// The code below to compute cr uses a similar pattern.
//
// Note that -11056 - 21712 + 32768 equals 0.
let mut cb = -11056 * r1 - 21712 * g1 + 32768 * b1 + (257 << 15)
if (cb.reinterpret_as_uint() & 0xff000000) == 0 {
cb = cb >> 16
} else {
cb = (cb >> 31).lnot()
}
// Note that 32768 - 27440 - 5328 equals 0.
let mut cr = 32768 * r1 - 27440 * g1 - 5328 * b1 + (257 << 15)
if (cr.reinterpret_as_uint() & 0xff000000) == 0 {
cr = cr >> 16
} else {
cr = (cr >> 31).lnot()
}
let yy = yy.to_byte()
let cb = cb.to_byte()
let cr = cr.to_byte()
(yy, cb, cr)
}
///|
/// y_cb_cr_to_rgb converts a Y'CbCr triple to an RGB triple.
pub fn y_cb_cr_to_rgb(y : Byte, cb : Byte, cr : Byte) -> (Byte, Byte, Byte) {
// The JFIF specification says:
// R = Y' + 1.40200*(Cr-128)
// G = Y' - 0.34414*(Cb-128) - 0.71414*(Cr-128)
// B = Y' + 1.77200*(Cb-128)
// https://www.w3.org/Graphics/JPEG/jfif3.pdf says Y but means Y'.
//
// Those formulae use non-integer multiplication factors. When computing,
// integer math is generally faster than floating point math. We multiply
// all of those factors by 1<<16 and round to the nearest integer:
// 91881 = roundToNearestInteger(1.40200 * 65536).
// 22554 = roundToNearestInteger(0.34414 * 65536).
// 46802 = roundToNearestInteger(0.71414 * 65536).
// 116130 = roundToNearestInteger(1.77200 * 65536).
//
// Adding a rounding adjustment in the range [0, 1<<16-1] and then shifting
// right by 16 gives us an integer math version of the original formulae.
// R = (65536*Y' + 91881 *(Cr-128) + adjustment) >> 16
// G = (65536*Y' - 22554 *(Cb-128) - 46802*(Cr-128) + adjustment) >> 16
// B = (65536*Y' + 116130 *(Cb-128) + adjustment) >> 16
// A constant rounding adjustment of 1<<15, one half of 1<<16, would mean
// round-to-nearest when dividing by 65536 (shifting right by 16).
// Similarly, a constant rounding adjustment of 0 would mean round-down.
//
// Defining YY1 = 65536*Y' + adjustment simplifies the formulae and
// requires fewer CPU operations:
// R = (YY1 + 91881 *(Cr-128) ) >> 16
// G = (YY1 - 22554 *(Cb-128) - 46802*(Cr-128)) >> 16
// B = (YY1 + 116130 *(Cb-128) ) >> 16
//
// The inputs (y, cb, cr) are 8 bit color, ranging in [0x00, 0xff]. In this
// function, the output is also 8 bit color, but in the related YCbCr.RGBA
// method, below, the output is 16 bit color, ranging in [0x0000, 0xffff].
// Outputting 16 bit color simply requires changing the 16 to 8 in the "R =
// etc >> 16" equation, and likewise for G and B.
//
// As mentioned above, a constant rounding adjustment of 1<<15 is a natural
// choice, but there is an additional constraint: if c0 := YCbCr{Y: y, Cb:
// 0x80, Cr: 0x80} and c1 := Gray{Y: y} then c0.RGBA() should equal
// c1.RGBA(). Specifically, if y == 0 then "R = etc >> 8" should yield
// 0x0000 and if y == 0xff then "R = etc >> 8" should yield 0xffff. If we
// used a constant rounding adjustment of 1<<15, then it would yield 0x0080
// and 0xff80 respectively.
//
// Note that when cb == 0x80 and cr == 0x80 then the formulae collapse to:
// R = YY1 >> n
// G = YY1 >> n
// B = YY1 >> n
// where n is 16 for this function (8 bit color output) and 8 for the
// YCbCr.RGBA method (16 bit color output).
//
// The solution is to make the rounding adjustment non-constant, and equal
// to 257*Y', which ranges over [0, 1<<16-1] as Y' ranges over [0, 255].
// YY1 is then defined as:
// YY1 = 65536*Y' + 257*Y'
// or equivalently:
// YY1 = Y' * 0x10101
let yy1 = y.to_int() * 0x10101
let cb1 = cb.to_int() - 128
let cr1 = cr.to_int() - 128
// The bit twiddling below is equivalent to
//
// r := (yy1 + 91881*cr1) >> 16
// if r < 0 {
// r = 0
// } else if r > 0xff {
// r = ^int32(0)
// }
//
// but uses fewer branches and is faster.
// Note that the uint8 type conversion in the return
// statement will convert ^int32(0) to 0xff.
// The code below to compute g and b uses a similar pattern.
let mut r = yy1 + 91881 * cr1
if (r.reinterpret_as_uint() & 0xff000000) == 0 {
r = r >> 16
} else {
r = (r >> 31).lnot()
}
let mut g = yy1 - 22554 * cb1 - 46802 * cr1
if (g.reinterpret_as_uint() & 0xff000000) == 0 {
g = g >> 16
} else {
g = (g >> 31).lnot()
}
let mut b = yy1 + 116130 * cb1
if (b.reinterpret_as_uint() & 0xff000000) == 0 {
b = b >> 16
} else {
b = (b >> 31).lnot()
}
let r = r.to_byte()
let g = g.to_byte()
let b = b.to_byte()
(r, g, b)
}
///|
/// YCbCr represents a fully opaque 24-bit Y'CbCr color, having 8 bits each for
/// one luma and two chroma components.
///
/// JPEG, VP8, the MPEG family and other codecs use this color model. Such
/// codecs often use the terms YUV and Y'CbCr interchangeably, but strictly
/// speaking, the term YUV applies only to analog video signals, and Y' (luma)
/// is Y (luminance) after applying gamma correction.
///
/// Conversion between RGB and Y'CbCr is lossy and there are multiple, slightly
/// different formulae for converting between the two. This package follows
/// the JFIF specification at https://www.w3.org/Graphics/JPEG/jfif3.pdf.
pub(all) struct YCbCr {
y : Byte
cb : Byte
cr : Byte
} derive(Eq)
///|
pub impl Show for YCbCr with fn output(self, logger) {
logger.write_string(
(
$|{y: \{self.y}, cb: \{self.cb}, cr: \{self.cr}}
),
)
}
///|
test "YCbCr show interface" {
let c = YCbCr::new(1, 2, 3)
inspect(
c,
content=(
#|{y: b'\x01', cb: b'\x02', cr: b'\x03'}
),
)
}
///|
/// `YCbCr` satisfies the `Color` trait.
let _YCbCr : &Color = YCbCr::new(0, 0, 0)
///|
pub fn YCbCr::new(y : Byte, cb : Byte, cr : Byte) -> YCbCr {
{ y, cb, cr }
}
///|
pub fn YCbCr::from(c : &Color) -> YCbCr {
if c.model() == "YCbCr" {
let (y, cb, cr, _) = c.raw()
return { y: y.to_byte(), cb: cb.to_byte(), cr: cr.to_byte() }
}
let (r, g, b, _) = c.rgba()
let r = (r >> 8).to_byte()
let g = (g >> 8).to_byte()
let b = (b >> 8).to_byte()
let (y, cb, cr) = rgb_to_y_cb_cr(r, g, b)
{ y, cb, cr }
}
///|
pub impl Color for YCbCr with fn model(_self) {
"YCbCr"
}
///|
pub impl Color for YCbCr with fn raw(self) {
(self.y.to_uint(), self.cb.to_uint(), self.cr.to_uint(), 0)
}
///|
pub impl Color for YCbCr with fn rgba(self) {
// This code is a copy of the y_cb_cr_to_rgb function above, except that it
// returns values in the range [0, 0xffff] instead of [0, 0xff]. There is a
// subtle difference between doing this and having YCbCr satisfy the Color
// interface by first converting to an RGBA. The latter loses some
// information by going to and from 8 bits per channel.
//
// For example, this code:
// const y, cb, cr = 0x7f, 0x7f, 0x7f
// r, g, b := color.y_cb_cr_to_rgb(y, cb, cr)
// r0, g0, b0, _ := color.YCbCr{y, cb, cr}.RGBA()
// r1, g1, b1, _ := color.RGBA{r, g, b, 0xff}.RGBA()
// fmt.Printf("0x%04x 0x%04x 0x%04x\n", r0, g0, b0)
// fmt.Printf("0x%04x 0x%04x 0x%04x\n", r1, g1, b1)
// prints:
// 0x7e18 0x808d 0x7db9
// 0x7e7e 0x8080 0x7d7d
let yy1 = self.y.to_int() * 0x10101
let cb1 = self.cb.to_int() - 128
let cr1 = self.cr.to_int() - 128
// The bit twiddling below is equivalent to
//
// r := (yy1 + 91881*cr1) >> 8
// if r < 0 {
// r = 0
// } else if r > 0xff {
// r = 0xffff
// }
//
// but uses fewer branches and is faster.
// The code below to compute g and b uses a similar pattern.
let mut r = yy1 + 91881 * cr1
if (r.reinterpret_as_uint() & 0xff000000) == 0 {
r = r >> 8
} else {
r = (r >> 31).lnot() & 0xffff
}
let mut g = yy1 - 22554 * cb1 - 46802 * cr1
if (g.reinterpret_as_uint() & 0xff000000) == 0 {
g = g >> 8
} else {
g = (g >> 31).lnot() & 0xffff
}
let mut b = yy1 + 116130 * cb1
if (b.reinterpret_as_uint() & 0xff000000) == 0 {
b = b >> 8
} else {
b = (b >> 31).lnot() & 0xffff
}
let r = r.reinterpret_as_uint()
let g = g.reinterpret_as_uint()
let b = b.reinterpret_as_uint()
(r, g, b, 0xffff)
}
///|
/// y_cb_cr_model is the [Model] for Y'CbCr colors.
pub let y_cb_cr_model : &Model = model_func(y_cb_cr_model_fn, "YCbCr", None)
///|
fn y_cb_cr_model_fn(c : &Color) -> &Color {
if c.model() == "YCbCr" {
return c
}
let (r, g, b, _) = c.rgba()
let r = (r >> 8).to_byte()
let g = (g >> 8).to_byte()
let b = (b >> 8).to_byte()
let (y, cb, cr) = rgb_to_y_cb_cr(r, g, b)
let c : YCbCr = { y, cb, cr }
c
}
///|
/// NYCbCrA represents a non-alpha-premultiplied Y'CbCr-with-alpha color, having
/// 8 bits each for one luma, two chroma and one alpha component.
pub(all) struct NYCbCrA {
y : Byte
cb : Byte
cr : Byte
a : Byte
} derive(Eq)
///|
pub impl Show for NYCbCrA with fn output(self, logger) {
logger.write_string(
(
$|{y: \{self.y}, cb: \{self.cb}, cr: \{self.cr}, a: \{self.a}}
),
)
}
///|
test "NYCbCrA show interface" {
let c = NYCbCrA::new(1, 2, 3, 255)
inspect(
c,
content=(
#|{y: b'\x01', cb: b'\x02', cr: b'\x03', a: b'\xFF'}
),
)
}
///|
/// `NYCbCrA` satisfies the `Color` trait.
let _NYCbCrA : &Color = NYCbCrA::new(0, 0, 0, 0)
///|
pub fn NYCbCrA::new(y : Byte, cb : Byte, cr : Byte, a : Byte) -> NYCbCrA {
{ y, cb, cr, a }
}
///|
pub impl Color for NYCbCrA with fn model(_self) {
"NYCbCrA"
}
///|
pub impl Color for NYCbCrA with fn raw(self) {
(self.y.to_uint(), self.cb.to_uint(), self.cr.to_uint(), self.a.to_uint())
}
///|
pub impl Color for NYCbCrA with fn rgba(self) {
// The first part of this method is the same as YCbCr.RGBA.
let yy1 = self.y.to_int() * 0x10101
let cb1 = self.cb.to_int() - 128
let cr1 = self.cr.to_int() - 128
// The bit twiddling below is equivalent to
//
// r := (yy1 + 91881*cr1) >> 8
// if r < 0 {
// r = 0
// } else if r > 0xff {
// r = 0xffff
// }
//
// but uses fewer branches and is faster.
// The code below to compute g and b uses a similar pattern.
let mut r = yy1 + 91881 * cr1
if (r.reinterpret_as_uint() & 0xff000000) == 0 {
r = r >> 8
} else {
r = (r >> 31).lnot() & 0xffff
}
let mut g = yy1 - 22554 * cb1 - 46802 * cr1
if (g.reinterpret_as_uint() & 0xff000000) == 0 {
g = g >> 8
} else {
g = (g >> 31).lnot() & 0xffff
}
let mut b = yy1 + 116130 * cb1
if (b.reinterpret_as_uint() & 0xff000000) == 0 {
b = b >> 8
} else {
b = (b >> 31).lnot() & 0xffff
}
// The second part of this method applies the alpha.
let a = self.a.to_uint() * 0x101
let r = r.reinterpret_as_uint() * a / 0xffff
let g = g.reinterpret_as_uint() * a / 0xffff
let b = b.reinterpret_as_uint() * a / 0xffff
(r, g, b, a)
}
///|
/// n_y_cb_cr_a_model is the [Model] for non-alpha-premultiplied Y'CbCr-with-alpha
/// colors.
pub let n_y_cb_cr_a_model : &Model = model_func(
n_y_cb_cr_a_model_fn,
"NYCbCrA",
None,
)
///|
fn n_y_cb_cr_a_model_fn(c : &Color) -> &Color {
if c.model() == "NYCbCrA" {
return c
}
let (r, g, b, a) = c.rgba()
let mut r = r
let mut g = g
let mut b = b
// Convert from alpha-premultiplied to non-alpha-premultiplied.
if a != 0 {
r = r * 0xffff / a
g = g * 0xffff / a
b = b * 0xffff / a
}
//
let r = (r >> 8).to_byte()
let g = (g >> 8).to_byte()
let b = (b >> 8).to_byte()
let a = (a >> 8).to_byte()
let (y, cb, cr) = rgb_to_y_cb_cr(r, g, b)
let c : NYCbCrA = { y, cb, cr, a }
c
}
///|
/// rgb_to_cmyk converts an RGB triple to a CMYK quadruple.
pub fn rgb_to_cmyk(r : Byte, g : Byte, b : Byte) -> (Byte, Byte, Byte, Byte) {
let rr = r.to_uint()
let gg = g.to_uint()
let bb = b.to_uint()
let mut w = rr
if w < gg {
w = gg
}
if w < bb {
w = bb
}
if w == 0 {
return (0, 0, 0, 0xff)
}
let c = ((w - rr) * 0xff / w).to_byte()
let m = ((w - gg) * 0xff / w).to_byte()
let y = ((w - bb) * 0xff / w).to_byte()
let k = (0xffU - w).to_byte()
(c, m, y, k)
}
///|
/// cmyk_to_rgb converts a [CMYK] quadruple to an RGB triple.
pub fn cmyk_to_rgb(
c : Byte,
m : Byte,
y : Byte,
k : Byte,
) -> (Byte, Byte, Byte) {
let w = 0xffffU - k.to_uint() * 0x101
let r = (0xffffU - c.to_uint() * 0x101) * w / 0xffff
let g = (0xffffU - m.to_uint() * 0x101) * w / 0xffff
let b = (0xffffU - y.to_uint() * 0x101) * w / 0xffff
let r = (r >> 8).to_byte()
let g = (g >> 8).to_byte()
let b = (b >> 8).to_byte()
(r, g, b)
}
///|
/// CMYK represents a fully opaque CMYK color, having 8 bits for each of cyan,
/// magenta, yellow and black.
///
/// It is not associated with any particular color profile.
pub(all) struct CMYK {
c : Byte
m : Byte
y : Byte
k : Byte
} derive(Eq)
///|
pub impl Show for CMYK with fn output(self, logger) {
logger.write_string(
(
$|{c: \{self.c}, m: \{self.m}, y: \{self.y}, k: \{self.k}}
),
)
}
///|
test "CMYK show interface" {
let c = CMYK::new(1, 2, 3, 255)
inspect(
c,
content=(
#|{c: b'\x01', m: b'\x02', y: b'\x03', k: b'\xFF'}
),
)
}
///|
/// `CMYK` satisfies the `Color` trait.
let _CMYK : &Color = CMYK::new(0, 0, 0, 0)
///|
pub fn CMYK::new(c : Byte, m : Byte, y : Byte, k : Byte) -> CMYK {
{ c, m, y, k }
}
///|
pub impl Color for CMYK with fn model(_self) {
"CMYK"
}
///|
pub impl Color for CMYK with fn raw(self) {
(self.c.to_uint(), self.m.to_uint(), self.y.to_uint(), self.k.to_uint())
}
///|
pub impl Color for CMYK with fn rgba(self) {
// This code is a copy of the cmyk_to_rgb function above, except that it
// returns values in the range [0, 0xffff] instead of [0, 0xff].
let w = 0xffffU - self.k.to_uint() * 0x101
let r = (0xffffU - self.c.to_uint() * 0x101) * w / 0xffff
let g = (0xffffU - self.m.to_uint() * 0x101) * w / 0xffff
let b = (0xffffU - self.y.to_uint() * 0x101) * w / 0xffff
(r, g, b, 0xffff)
}
///|
/// cmyk_model is the [Model] for CMYK colors.
pub let cmyk_model : &Model = model_func(cmyk_model_fn, "CMYK", None)
///|
fn cmyk_model_fn(c : &Color) -> &Color {
CMYK::from(c)
}
///|
pub fn CMYK::from(c : &Color) -> CMYK {
if c.model() == "CMYK" {
let (c, m, y, k) = c.raw()
return { c: c.to_byte(), m: m.to_byte(), y: y.to_byte(), k: k.to_byte() }
}
let (r, g, b, _) = c.rgba()
let r = (r >> 8).to_byte()
let g = (g >> 8).to_byte()
let b = (b >> 8).to_byte()
let (c, m, y, k) = rgb_to_cmyk(r, g, b)
{ c, m, y, k }
}