File indexing completed on 2024-05-05 04:46:57

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