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"