FastLED 3.10.6
Loading...
Searching...
No Matches
color.cpp.hpp
Go to the documentation of this file.
1// ok no header - implementation for fl/fled/color.h.
2
3#include "fl/fled/color.h"
5
7#include "fl/stl/cstring.h"
8#include "fl/stl/json.h"
9#include "fl/stl/string.h"
10
11namespace fl {
12namespace fled {
13
14namespace {
15
16// Recognized transfer names. Pq/Hlg are spec-reserved: named so the
17// diagnostic can say "reserved" instead of "unrecognized", but rejected by
18// every v1 pixel format.
20
21bool nameIs(const fl::string& s, const char* lit) FL_NO_EXCEPT {
22 return fl::strcmp(s.c_str(), lit) == 0;
23}
24
26 if (nameIs(s, "srgb")) return TransferName::Srgb;
27 if (nameIs(s, "bt709")) return TransferName::Bt709;
28 if (nameIs(s, "linear")) return TransferName::Linear;
29 if (nameIs(s, "pq")) return TransferName::Pq;
30 if (nameIs(s, "hlg")) return TransferName::Hlg;
32}
33
34// Matrix names that standard video metadata defines but that v1 reserves:
35// they describe YCbCr coefficient sets, and no v1 pixel format carries YCbCr.
36// Distinguished from outright-unknown values so the diagnostic can say
37// "reserved" (look it up in the spec) rather than "unrecognized" (you typoed).
39 static const char* const kReserved[] = {
40 "bt709", "bt601", "bt470bg", "smpte170m", "bt2020ncl", "bt2020cl",
41 "ycbcr", "ycgco", "fcc",
42 };
43 for (fl::size i = 0; i < sizeof(kReserved) / sizeof(kReserved[0]); ++i) {
44 if (nameIs(s, kReserved[i])) return true;
45 }
46 return false;
47}
48
49// Display-encoded RGB family: rgb8, rgba8, rgb565le.
51 return pf == static_cast<fl::u8>(PixelFormat::Rgb8) ||
52 pf == static_cast<fl::u8>(PixelFormat::Rgba8) ||
53 pf == static_cast<fl::u8>(PixelFormat::Rgb565Le);
54}
55
57 return pf == static_cast<fl::u8>(PixelFormat::Rgb16Linear);
58}
59
60// True for a real, ordinary number: not NaN, not an infinity.
61//
62// `NaN != NaN` is the definition; the magnitude bound catches both
63// infinities without needing <cmath>, and 1e30 is far outside any
64// chromaticity while remaining exactly representable.
66 if (v != v) return false;
67 return v > -1e30f && v < 1e30f;
68}
69
70// Reads one CIE xy pair: a 2-element array of numbers.
71//
72// Shape alone is not admission (FastLED#4156 R6). Two properties are checked
73// here because neither is a matter of taste:
74//
75// * the values must be ordinary numbers -- a JSON `1e400` parses to an
76// infinity, and an infinite coordinate is not a chromaticity;
77// * `y` must be positive, since xyY -> XYZ divides by it. At y = 0 the
78// conversion yields a zero column, and the profile is refused later as a
79// singular matrix -- correct, but reported far from the field that
80// caused it.
81//
82// Deliberately NOT checked: whether the pair lies inside the xy simplex.
83// Primaries outside it are imaginary, wide-gamut encodings legitimately use
84// them, and R6 asks for that to be an explicit decision rather than a
85// side effect of a range check. It has not been made, so nothing here
86// forecloses it.
87bool readXy(const fl::json& node, float* outX, float* outY) FL_NO_EXCEPT {
88 if (!node.is_array()) return false;
89 // Exactly two, not at least two: [0.64, 0.33, 1] would otherwise resolve
90 // while silently dropping the third item. ledmapper's validator already
91 // requires exactly two, and the two must agree.
92 if (node.size() != 2) return false;
93 auto x = node[static_cast<fl::size>(0)].as_float();
94 auto y = node[static_cast<fl::size>(1)].as_float();
95 if (!x || !y) return false;
96 const float fx = static_cast<float>(*x);
97 const float fy = static_cast<float>(*y);
98 if (!isOrdinaryNumber(fx) || !isOrdinaryNumber(fy)) return false;
99 if (fy <= 0.0f) return false;
100 *outX = fx;
101 *outY = fy;
102 return true;
103}
104
105// Custom primaries object: red/green/blue/white, each a CIE xy pair.
106bool readCustomPrimaries(const fl::json& node, float* out8) FL_NO_EXCEPT {
107 static const char* const kKeys[4] = {"red", "green", "blue", "white"};
108 for (fl::size i = 0; i < 4; ++i) {
109 const fl::string key(kKeys[i]);
110 if (!node.contains(key)) return false;
111 if (!readXy(node[key], &out8[i * 2], &out8[i * 2 + 1])) return false;
112 }
113 return true;
114}
115
116} // namespace
117
119 return isDisplayEncodedRgb(pixelFormat) || isLinearRgb(pixelFormat);
120}
121
123 if (!out) return false;
124 if (!pixelFormatHasDefaultTuple(pixelFormat)) return false;
125 out->primaries = ColorPrimaries::Bt709;
126 out->transfer = isLinearRgb(pixelFormat) ? ColorTransfer::Linear
128 out->matrix = ColorMatrix::Rgb;
129 out->range = ColorRange::Full;
130 out->declared = false;
131 for (fl::size i = 0; i < 8; ++i) {
132 out->customPrimaries[i] = 0.0f;
133 }
134 return true;
135}
136
137ColorStatus resolveVideoColor(const fl::json& envelope, fl::u8 pixelFormat,
139 if (!out) return ColorStatus::NullOutput;
140
141 const bool hasTuple = pixelFormatHasDefaultTuple(pixelFormat);
142
143 // Locate video.color without requiring either level to exist.
144 const fl::string kVideo("video");
145 const fl::string kColor("color");
146 bool present = false;
147 fl::json colorNode;
148 if (envelope.is_object() && envelope.contains(kVideo)) {
149 const fl::json videoNode = envelope[kVideo];
150 if (videoNode.is_object() && videoNode.contains(kColor)) {
151 colorNode = videoNode[kColor];
152 // An explicit JSON null means "not set" for most serializers.
153 // Treat it as absent rather than rejecting the file - omitting
154 // the key and nulling it should not have opposite outcomes.
155 present = !colorNode.is_null();
156 }
157 }
158
159 if (!present) {
160 // Absent metadata: the format's default tuple is the historical
161 // interpretation. Formats without one are simply unresolvable.
162 VideoColor resolved;
163 if (!defaultVideoColor(pixelFormat, &resolved)) {
165 }
166 *out = resolved;
167 return ColorStatus::Ok;
168 }
169
170 if (!colorNode.is_object()) {
172 }
173
174 const fl::string kPrimaries("primaries");
175 const fl::string kTransfer("transfer");
176 const fl::string kMatrix("matrix");
177 const fl::string kRange("range");
178
179 const bool hasP = colorNode.contains(kPrimaries);
180 const bool hasT = colorNode.contains(kTransfer);
181 const bool hasM = colorNode.contains(kMatrix);
182 const bool hasR = colorNode.contains(kRange);
183
184 // Key inheritance is scoped to formats that define a default tuple.
185 // Everything else is all-or-nothing, so a future YCbCr format can never
186 // silently inherit `matrix: rgb`.
187 if (!hasTuple && !(hasP && hasT && hasM && hasR)) {
189 }
190
191 VideoColor resolved;
192 if (!defaultVideoColor(pixelFormat, &resolved)) {
193 // No tuple to seed from; every field is explicit here (checked above).
195 resolved.transfer = ColorTransfer::Srgb;
196 resolved.matrix = ColorMatrix::Rgb;
197 resolved.range = ColorRange::Full;
198 for (fl::size i = 0; i < 8; ++i) resolved.customPrimaries[i] = 0.0f;
199 }
200 resolved.declared = true;
201
202 // ---- primaries ----
203 if (hasP) {
204 const fl::json node = colorNode[kPrimaries];
205 if (node.is_object()) {
206 if (!readCustomPrimaries(node, resolved.customPrimaries)) {
208 }
210 } else if (node.is_string()) {
211 auto s = node.as_string();
212 if (!s) return ColorStatus::UnknownPrimaries;
213 if (nameIs(*s, "bt709")) resolved.primaries = ColorPrimaries::Bt709;
214 else if (nameIs(*s, "display-p3")) resolved.primaries = ColorPrimaries::DisplayP3;
215 else if (nameIs(*s, "bt2020")) resolved.primaries = ColorPrimaries::Bt2020;
217 } else {
219 }
220 }
221
222 // ---- transfer ----
223 if (hasT) {
224 const fl::json node = colorNode[kTransfer];
225 if (!node.is_string()) return ColorStatus::UnknownTransfer;
226 auto s = node.as_string();
227 if (!s) return ColorStatus::UnknownTransfer;
228 switch (parseTransferName(*s)) {
229 case TransferName::Srgb: resolved.transfer = ColorTransfer::Srgb; break;
230 case TransferName::Bt709: resolved.transfer = ColorTransfer::Bt709; break;
231 case TransferName::Linear: resolved.transfer = ColorTransfer::Linear; break;
232 case TransferName::Pq:
233 case TransferName::Hlg:
234 // Reserved names: no v1 payload format can carry them.
236 case TransferName::Unrecognized:
238 }
239 }
240
241 // Transfer must agree with what the pixel format's bytes actually hold.
242 if (isDisplayEncodedRgb(pixelFormat) &&
243 resolved.transfer == ColorTransfer::Linear) {
245 }
246 if (isLinearRgb(pixelFormat) && resolved.transfer != ColorTransfer::Linear) {
248 }
249
250 // ---- matrix ----
251 if (hasM) {
252 const fl::json node = colorNode[kMatrix];
253 if (!node.is_string()) return ColorStatus::UnknownMatrix;
254 auto s = node.as_string();
255 if (!s) return ColorStatus::UnknownMatrix;
256 if (!nameIs(*s, "rgb")) {
257 // "reserved" sends the author to the spec; "unrecognized" tells
258 // them they typoed. Conflating the two wastes their time.
259 return isReservedMatrixName(*s) ? ColorStatus::MatrixUnsupported
261 }
262 resolved.matrix = ColorMatrix::Rgb;
263 }
264
265 // ---- range ----
266 if (hasR) {
267 const fl::json node = colorNode[kRange];
268 if (!node.is_string()) return ColorStatus::UnknownRange;
269 auto s = node.as_string();
270 if (!s) return ColorStatus::UnknownRange;
271 if (nameIs(*s, "full")) {
272 resolved.range = ColorRange::Full;
273 } else if (nameIs(*s, "limited")) {
275 } else {
277 }
278 }
279
280 *out = resolved;
281 return ColorStatus::Ok;
282}
283
286 return resolveVideoColor(envelope, static_cast<fl::u8>(pixelFormat), out);
287}
288
289bool toFledPixelFormat(const PixelStorage& storage, const VideoColor& color,
291 if (!out) {
292 return false;
293 }
294 const bool validPrimaries = color.primaries == ColorPrimaries::Bt709 ||
295 color.primaries == ColorPrimaries::DisplayP3 ||
296 color.primaries == ColorPrimaries::Bt2020 ||
297 color.primaries == ColorPrimaries::Custom;
298 const bool validTransfer = color.transfer == ColorTransfer::Srgb ||
299 color.transfer == ColorTransfer::Bt709 ||
300 color.transfer == ColorTransfer::Linear;
301 if (!validPrimaries || !validTransfer || color.matrix != ColorMatrix::Rgb ||
302 color.range != ColorRange::Full) {
303 return false;
304 }
305 if (color.primaries == ColorPrimaries::Custom) {
306 for (fl::size_t i = 0; i < 8; ++i) {
307 const float value = color.customPrimaries[i];
308 if (!(value == value) || value <= 0.0f || value > 1.0f) {
309 return false;
310 }
311 }
312 }
313 switch (storage.mFormat) {
315 if (storage.mComponentByteOrder != ComponentByteOrder::NotApplicable ||
316 color.transfer == ColorTransfer::Linear) {
317 return false;
318 }
319 *out = PixelFormat::Rgb8;
320 return true;
322 if (storage.mComponentByteOrder != ComponentByteOrder::LittleEndian ||
323 color.transfer != ColorTransfer::Linear) {
324 return false;
325 }
327 return true;
328 default:
329 return false;
330 }
331}
332
334 switch (status) {
335 case ColorStatus::Ok:
336 return "ok";
338 return "resolveVideoColor called with a null out-pointer";
340 return "video.color is absent and this pixel_format defines no default color tuple";
342 return "video.color must be a JSON object";
344 return "video.color.primaries is not a recognized name or custom xy object";
346 return "video.color.transfer is not a recognized transfer function";
348 return "video.color.matrix is not a recognized value";
350 return "video.color.range is not a recognized value";
352 return "video.color.transfer conflicts with the header pixel_format";
354 return "video.color.range \"limited\" is reserved and unsupported in v1";
356 return "video.color.matrix values other than \"rgb\" are reserved in v1";
358 return "this pixel_format defines no default tuple: video.color must declare all four keys or be absent";
360 return "video.color.primaries object needs red/green/blue/white CIE xy pairs";
361 }
362 return "unknown color status";
363}
364
366 if (out == nullptr) {
367 return false;
368 }
370 switch (color.primaries) {
373 break;
376 break;
378 primaries = SourceProfile::bt2020().primaries;
379 break;
381 primaries = RgbPrimaries(
382 Chromaticity(color.customPrimaries[0], color.customPrimaries[1]),
383 Chromaticity(color.customPrimaries[2], color.customPrimaries[3]),
384 Chromaticity(color.customPrimaries[4], color.customPrimaries[5]),
385 Chromaticity(color.customPrimaries[6], color.customPrimaries[7]));
386 break;
387 }
389 switch (color.transfer) {
391 transfer = TransferFunction::Srgb;
392 break;
394 transfer = TransferFunction::Bt709;
395 break;
397 transfer = TransferFunction::Linear;
398 break;
399 }
400 // matrix and range: v1 defines only rgb / full, which is what the
401 // pipeline assumes, so there is nothing further to carry.
402 *out = SourceProfile(primaries, transfer);
403 return true;
404}
405
406} // namespace fled
407} // namespace fl
bool is_object() const FL_NO_EXCEPT
Definition json.h:391
fl::optional< fl::string > as_string() const FL_NO_EXCEPT
Definition json.h:443
bool is_null() const FL_NO_EXCEPT
Definition json.h:381
bool contains(size_t idx) const FL_NO_EXCEPT
Definition json.h:790
bool is_string() const FL_NO_EXCEPT
Definition json.h:388
FastLED's Elegant JSON Library: fl::json
__SIZE_TYPE__ size_t
Definition s16x16x4.h:16
bool isReservedMatrixName(const fl::string &s) FL_NO_EXCEPT
Definition color.cpp.hpp:38
bool isDisplayEncodedRgb(fl::u8 pf) FL_NO_EXCEPT
Definition color.cpp.hpp:50
bool isOrdinaryNumber(float v) FL_NO_EXCEPT
Definition color.cpp.hpp:65
bool readCustomPrimaries(const fl::json &node, float *out8) FL_NO_EXCEPT
bool isLinearRgb(fl::u8 pf) FL_NO_EXCEPT
Definition color.cpp.hpp:56
TransferName parseTransferName(const fl::string &s) FL_NO_EXCEPT
Definition color.cpp.hpp:25
bool nameIs(const fl::string &s, const char *lit) FL_NO_EXCEPT
Definition color.cpp.hpp:21
bool readXy(const fl::json &node, float *outX, float *outY) FL_NO_EXCEPT
Definition color.cpp.hpp:87
bool toFledPixelFormat(const PixelStorage &storage, const VideoColor &color, PixelFormat *out) FL_NO_EXCEPT
bool defaultVideoColor(fl::u8 pixelFormat, VideoColor *out) FL_NO_EXCEPT
ColorStatus resolveVideoColor(const fl::json &envelope, fl::u8 pixelFormat, VideoColor *out) FL_NO_EXCEPT
bool toSourceProfile(const VideoColor &color, SourceProfile *out) FL_NO_EXCEPT
ColorStatus
Definition color.h:54
@ TransferConflictsWithFormat
Definition color.h:70
bool pixelFormatHasDefaultTuple(fl::u8 pixelFormat) FL_NO_EXCEPT
const char * colorStatusMessage(ColorStatus status) FL_NO_EXCEPT
ColorMatrix matrix
Definition color.h:87
ColorPrimaries primaries
Definition color.h:85
ColorRange range
Definition color.h:88
ColorTransfer transfer
Definition color.h:86
float customPrimaries[8]
Definition color.h:92
unsigned char u8
Definition stdint.h:131
constexpr int type_rank< T >::value
int strcmp(const char *s1, const char *s2) FL_NO_EXCEPT
TransferFunction
InputGamut g FL_NO_EXCEPT
Definition rgbw.h:121
Base definition for an LED controller.
Definition crgb.hpp:179
CIE 1931 xy chromaticity, independent of encoded storage and chipset.
RgbPrimaries primaries
static constexpr SourceProfile srgbBt709() FL_NO_EXCEPT
static constexpr SourceProfile bt2020() FL_NO_EXCEPT
static constexpr SourceProfile displayP3() FL_NO_EXCEPT
A source declaration, not a container or an output-device selection.