File indexing completed on 2024-04-14 03:53:44

0001 /*
0002  *  SPDX-FileCopyrightText: 2020 Carson Black <uhhadd@gmail.com>
0003  *
0004  *  SPDX-License-Identifier: LGPL-2.0-or-later
0005  */
0006 
0007 #include "colorutils.h"
0008 
0009 #include "loggingcategory.h"
0010 #include <QIcon>
0011 #include <QtMath>
0012 #include <cmath>
0013 #include <map>
0014 
0015 ColorUtils::ColorUtils(QObject *parent)
0016     : QObject(parent)
0017 {
0018 }
0019 
0020 ColorUtils::Brightness ColorUtils::brightnessForColor(const QColor &color)
0021 {
0022     auto luma = [](const QColor &color) {
0023         return (0.299 * color.red() + 0.587 * color.green() + 0.114 * color.blue()) / 255;
0024     };
0025 
0026     return luma(color) > 0.5 ? ColorUtils::Brightness::Light : ColorUtils::Brightness::Dark;
0027 }
0028 
0029 qreal ColorUtils::grayForColor(const QColor &color)
0030 {
0031     return (0.299 * color.red() + 0.587 * color.green() + 0.114 * color.blue()) / 255;
0032 }
0033 
0034 QColor ColorUtils::alphaBlend(const QColor &foreground, const QColor &background)
0035 {
0036     const auto foregroundAlpha = foreground.alpha();
0037     const auto inverseForegroundAlpha = 0xff - foregroundAlpha;
0038     const auto backgroundAlpha = background.alpha();
0039 
0040     if (foregroundAlpha == 0x00) {
0041         return background;
0042     }
0043 
0044     if (backgroundAlpha == 0xff) {
0045         return QColor::fromRgb((foregroundAlpha * foreground.red()) + (inverseForegroundAlpha * background.red()),
0046                                (foregroundAlpha * foreground.green()) + (inverseForegroundAlpha * background.green()),
0047                                (foregroundAlpha * foreground.blue()) + (inverseForegroundAlpha * background.blue()),
0048                                0xff);
0049     } else {
0050         const auto inverseBackgroundAlpha = (backgroundAlpha * inverseForegroundAlpha) / 255;
0051         const auto finalAlpha = foregroundAlpha + inverseBackgroundAlpha;
0052         Q_ASSERT(finalAlpha != 0x00);
0053         return QColor::fromRgb((foregroundAlpha * foreground.red()) + (inverseBackgroundAlpha * background.red()),
0054                                (foregroundAlpha * foreground.green()) + (inverseBackgroundAlpha * background.green()),
0055                                (foregroundAlpha * foreground.blue()) + (inverseBackgroundAlpha * background.blue()),
0056                                finalAlpha);
0057     }
0058 }
0059 
0060 QColor ColorUtils::linearInterpolation(const QColor &one, const QColor &two, double balance)
0061 {
0062     auto linearlyInterpolateDouble = [](double one, double two, double factor) {
0063         return one + (two - one) * factor;
0064     };
0065 
0066     // QColor returns -1 when hue is undefined, which happens whenever
0067     // saturation is 0. When this happens, interpolation can go wrong so handle
0068     // it by first trying to use the other color's hue and if that is also -1,
0069     // just skip the hue interpolation by using 0 for both.
0070     auto sourceHue = std::max(one.hueF() > 0.0 ? one.hueF() : two.hueF(), 0.0f);
0071     auto targetHue = std::max(two.hueF() > 0.0 ? two.hueF() : one.hueF(), 0.0f);
0072 
0073     auto hue = std::fmod(linearlyInterpolateDouble(sourceHue, targetHue, balance), 1.0);
0074     auto saturation = std::clamp(linearlyInterpolateDouble(one.saturationF(), two.saturationF(), balance), 0.0, 1.0);
0075     auto value = std::clamp(linearlyInterpolateDouble(one.valueF(), two.valueF(), balance), 0.0, 1.0);
0076     auto alpha = std::clamp(linearlyInterpolateDouble(one.alphaF(), two.alphaF(), balance), 0.0, 1.0);
0077 
0078     return QColor::fromHsvF(hue, saturation, value, alpha);
0079 }
0080 
0081 // Some private things for the adjust, change, and scale properties
0082 struct ParsedAdjustments {
0083     double red = 0.0;
0084     double green = 0.0;
0085     double blue = 0.0;
0086 
0087     double hue = 0.0;
0088     double saturation = 0.0;
0089     double value = 0.0;
0090 
0091     double alpha = 0.0;
0092 };
0093 
0094 ParsedAdjustments parseAdjustments(const QJSValue &value)
0095 {
0096     ParsedAdjustments parsed;
0097 
0098     auto checkProperty = [](const QJSValue &value, const QString &property) {
0099         if (value.hasProperty(property)) {
0100             auto val = value.property(property);
0101             if (val.isNumber()) {
0102                 return QVariant::fromValue(val.toNumber());
0103             }
0104         }
0105         return QVariant();
0106     };
0107 
0108     std::vector<std::pair<QString, double &>> items{{QStringLiteral("red"), parsed.red},
0109                                                     {QStringLiteral("green"), parsed.green},
0110                                                     {QStringLiteral("blue"), parsed.blue},
0111                                                     //
0112                                                     {QStringLiteral("hue"), parsed.hue},
0113                                                     {QStringLiteral("saturation"), parsed.saturation},
0114                                                     {QStringLiteral("value"), parsed.value},
0115                                                     //
0116                                                     {QStringLiteral("alpha"), parsed.alpha}};
0117 
0118     for (const auto &item : items) {
0119         auto val = checkProperty(value, item.first);
0120         if (val.isValid()) {
0121             item.second = val.toDouble();
0122         }
0123     }
0124 
0125     if ((parsed.red || parsed.green || parsed.blue) && (parsed.hue || parsed.saturation || parsed.value)) {
0126         qCCritical(KirigamiLog) << "It is an error to have both RGB and HSV values in an adjustment.";
0127     }
0128 
0129     return parsed;
0130 }
0131 
0132 QColor ColorUtils::adjustColor(const QColor &color, const QJSValue &adjustments)
0133 {
0134     auto adjusts = parseAdjustments(adjustments);
0135 
0136     if (qBound(-360.0, adjusts.hue, 360.0) != adjusts.hue) {
0137         qCCritical(KirigamiLog) << "Hue is out of bounds";
0138     }
0139     if (qBound(-255.0, adjusts.red, 255.0) != adjusts.red) {
0140         qCCritical(KirigamiLog) << "Red is out of bounds";
0141     }
0142     if (qBound(-255.0, adjusts.green, 255.0) != adjusts.green) {
0143         qCCritical(KirigamiLog) << "Green is out of bounds";
0144     }
0145     if (qBound(-255.0, adjusts.blue, 255.0) != adjusts.blue) {
0146         qCCritical(KirigamiLog) << "Green is out of bounds";
0147     }
0148     if (qBound(-255.0, adjusts.saturation, 255.0) != adjusts.saturation) {
0149         qCCritical(KirigamiLog) << "Saturation is out of bounds";
0150     }
0151     if (qBound(-255.0, adjusts.value, 255.0) != adjusts.value) {
0152         qCCritical(KirigamiLog) << "Value is out of bounds";
0153     }
0154     if (qBound(-255.0, adjusts.alpha, 255.0) != adjusts.alpha) {
0155         qCCritical(KirigamiLog) << "Alpha is out of bounds";
0156     }
0157 
0158     auto copy = color;
0159 
0160     if (adjusts.alpha) {
0161         copy.setAlpha(qBound(0.0, copy.alpha() + adjusts.alpha, 255.0));
0162     }
0163 
0164     if (adjusts.red || adjusts.green || adjusts.blue) {
0165         copy.setRed(qBound(0.0, copy.red() + adjusts.red, 255.0));
0166         copy.setGreen(qBound(0.0, copy.green() + adjusts.green, 255.0));
0167         copy.setBlue(qBound(0.0, copy.blue() + adjusts.blue, 255.0));
0168     } else if (adjusts.hue || adjusts.saturation || adjusts.value) {
0169         copy.setHsv(std::fmod(copy.hue() + adjusts.hue, 360.0),
0170                     qBound(0.0, copy.saturation() + adjusts.saturation, 255.0),
0171                     qBound(0.0, copy.value() + adjusts.value, 255.0),
0172                     copy.alpha());
0173     }
0174 
0175     return copy;
0176 }
0177 
0178 QColor ColorUtils::scaleColor(const QColor &color, const QJSValue &adjustments)
0179 {
0180     auto adjusts = parseAdjustments(adjustments);
0181     auto copy = color;
0182 
0183     if (qBound(-100.0, adjusts.red, 100.00) != adjusts.red) {
0184         qCCritical(KirigamiLog) << "Red is out of bounds";
0185     }
0186     if (qBound(-100.0, adjusts.green, 100.00) != adjusts.green) {
0187         qCCritical(KirigamiLog) << "Green is out of bounds";
0188     }
0189     if (qBound(-100.0, adjusts.blue, 100.00) != adjusts.blue) {
0190         qCCritical(KirigamiLog) << "Blue is out of bounds";
0191     }
0192     if (qBound(-100.0, adjusts.saturation, 100.00) != adjusts.saturation) {
0193         qCCritical(KirigamiLog) << "Saturation is out of bounds";
0194     }
0195     if (qBound(-100.0, adjusts.value, 100.00) != adjusts.value) {
0196         qCCritical(KirigamiLog) << "Value is out of bounds";
0197     }
0198     if (qBound(-100.0, adjusts.alpha, 100.00) != adjusts.alpha) {
0199         qCCritical(KirigamiLog) << "Alpha is out of bounds";
0200     }
0201 
0202     if (adjusts.hue != 0) {
0203         qCCritical(KirigamiLog) << "Hue cannot be scaled";
0204     }
0205 
0206     auto shiftToAverage = [](double current, double factor) {
0207         auto scale = qBound(-100.0, factor, 100.0) / 100;
0208         return current + (scale > 0 ? 255 - current : current) * scale;
0209     };
0210 
0211     if (adjusts.alpha) {
0212         copy.setAlpha(qBound(0.0, shiftToAverage(copy.alpha(), adjusts.alpha), 255.0));
0213     }
0214 
0215     if (adjusts.red || adjusts.green || adjusts.blue) {
0216         copy.setRed(qBound(0.0, shiftToAverage(copy.red(), adjusts.red), 255.0));
0217         copy.setGreen(qBound(0.0, shiftToAverage(copy.green(), adjusts.green), 255.0));
0218         copy.setBlue(qBound(0.0, shiftToAverage(copy.blue(), adjusts.blue), 255.0));
0219     } else {
0220         copy.setHsv(copy.hue(),
0221                     qBound(0.0, shiftToAverage(copy.saturation(), adjusts.saturation), 255.0),
0222                     qBound(0.0, shiftToAverage(copy.value(), adjusts.value), 255.0),
0223                     copy.alpha());
0224     }
0225 
0226     return copy;
0227 }
0228 
0229 QColor ColorUtils::tintWithAlpha(const QColor &targetColor, const QColor &tintColor, double alpha)
0230 {
0231     qreal tintAlpha = tintColor.alphaF() * alpha;
0232     qreal inverseAlpha = 1.0 - tintAlpha;
0233 
0234     if (qFuzzyCompare(tintAlpha, 1.0)) {
0235         return tintColor;
0236     } else if (qFuzzyIsNull(tintAlpha)) {
0237         return targetColor;
0238     }
0239 
0240     return QColor::fromRgbF(tintColor.redF() * tintAlpha + targetColor.redF() * inverseAlpha,
0241                             tintColor.greenF() * tintAlpha + targetColor.greenF() * inverseAlpha,
0242                             tintColor.blueF() * tintAlpha + targetColor.blueF() * inverseAlpha,
0243                             tintAlpha + inverseAlpha * targetColor.alphaF());
0244 }
0245 
0246 ColorUtils::XYZColor ColorUtils::colorToXYZ(const QColor &color)
0247 {
0248     // http://wiki.nuaj.net/index.php/Color_Transforms#RGB_.E2.86.92_XYZ
0249     qreal r = color.redF();
0250     qreal g = color.greenF();
0251     qreal b = color.blueF();
0252     // Apply gamma correction (i.e. conversion to linear-space)
0253     auto correct = [](qreal &v) {
0254         if (v > 0.04045) {
0255             v = std::pow((v + 0.055) / 1.055, 2.4);
0256         } else {
0257             v = v / 12.92;
0258         }
0259     };
0260 
0261     correct(r);
0262     correct(g);
0263     correct(b);
0264 
0265     // Observer. = 2°, Illuminant = D65
0266     const qreal x = r * 0.4124 + g * 0.3576 + b * 0.1805;
0267     const qreal y = r * 0.2126 + g * 0.7152 + b * 0.0722;
0268     const qreal z = r * 0.0193 + g * 0.1192 + b * 0.9505;
0269 
0270     return XYZColor{x, y, z};
0271 }
0272 
0273 ColorUtils::LabColor ColorUtils::colorToLab(const QColor &color)
0274 {
0275     // First: convert to XYZ
0276     const auto xyz = colorToXYZ(color);
0277 
0278     // Second: convert from XYZ to L*a*b
0279     qreal x = xyz.x / 0.95047; // Observer= 2°, Illuminant= D65
0280     qreal y = xyz.y / 1.0;
0281     qreal z = xyz.z / 1.08883;
0282 
0283     auto pivot = [](qreal &v) {
0284         if (v > 0.008856) {
0285             v = std::pow(v, 1.0 / 3.0);
0286         } else {
0287             v = (7.787 * v) + (16.0 / 116.0);
0288         }
0289     };
0290 
0291     pivot(x);
0292     pivot(y);
0293     pivot(z);
0294 
0295     LabColor labColor;
0296     labColor.l = std::max(0.0, (116 * y) - 16);
0297     labColor.a = 500 * (x - y);
0298     labColor.b = 200 * (y - z);
0299 
0300     return labColor;
0301 }
0302 
0303 qreal ColorUtils::chroma(const QColor &color)
0304 {
0305     LabColor labColor = colorToLab(color);
0306 
0307     // Chroma is hypotenuse of a and b
0308     return sqrt(pow(labColor.a, 2) + pow(labColor.b, 2));
0309 }
0310 
0311 qreal ColorUtils::luminance(const QColor &color)
0312 {
0313     const auto &xyz = colorToXYZ(color);
0314     // Luminance is equal to Y
0315     return xyz.y;
0316 }
0317 
0318 #include "moc_colorutils.cpp"