File indexing completed on 2024-04-28 15:27:40

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