File indexing completed on 2024-12-01 07:26:15

0001 /*
0002  * SPDX-FileCopyrightText: 2009 Kare Sars <kare dot sars at iki dot fi>
0003  * SPDX-FileCopyrightText: 2014 Gregor Mitsch : port to KDE5 frameworks
0004  *
0005  * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0006  */
0007 
0008 #include "gammaoption.h"
0009 
0010 #include <QVarLengthArray>
0011 
0012 #include <ksanecore_debug.h>
0013 
0014 #include <cmath>
0015 
0016 namespace KSaneCore
0017 {
0018 
0019 GammaOption::GammaOption(const SANE_Handle handle, const int index)
0020     : BaseOption(handle, index)
0021 {
0022     m_optionType = Option::TypeGamma;
0023 }
0024 
0025 bool GammaOption::setValue(const QVariant &value)
0026 {
0027     if (state() == Option::StateHidden) {
0028         return false;
0029     }
0030 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0031     if (static_cast<QMetaType::Type>(value.type()) == QMetaType::QString) {
0032 #else
0033     if (value.userType() == QMetaType::QString) {
0034 #endif
0035         const QString stringValue = value.toString();
0036         QStringList gammaValues;
0037         int brightness;
0038         int contrast;
0039         int gamma;
0040         bool ok = true;
0041 
0042         gammaValues = stringValue.split(QLatin1Char(':'));
0043         if (gammaValues.size() != 3) {
0044             return false;
0045         }
0046         brightness = gammaValues.at(0).toInt(&ok);
0047         if (ok) {
0048             contrast = gammaValues.at(1).toInt(&ok);
0049         }
0050         if (ok) {
0051             gamma = gammaValues.at(2).toInt(&ok);
0052         }
0053 
0054         if (ok && (m_brightness != brightness || m_contrast != contrast || m_gamma != gamma) ) {
0055             m_brightness = brightness;
0056             m_contrast = contrast;
0057             m_gamma = gamma;
0058             calculateGTwriteData();
0059         }
0060         return true;
0061     }
0062     if (value.canConvert<QVariantList>()) { // It's a list
0063         QVariantList copy = value.toList();
0064 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0065         if (copy.size() != 3 || static_cast<QMetaType::Type>(copy.at(0).type()) != QMetaType::Int
0066             || static_cast<QMetaType::Type>(copy.at(1).type()) != QMetaType::Int || static_cast<QMetaType::Type>(copy.at(2).type()) != QMetaType::Int) {
0067 #else
0068         if (copy.size() != 3 || copy.at(0).userType() != QMetaType::Int || copy.at(1).userType() != QMetaType::Int || copy.at(2).userType() != QMetaType::Int) {
0069 #endif
0070             return false;
0071         }
0072         if (m_brightness != copy.at(0).toInt() || m_contrast != copy.at(1).toInt() || m_gamma != copy.at(2).toInt() ) {
0073             m_brightness = copy.at(0).toInt();
0074             m_contrast = copy.at(1).toInt();
0075             m_gamma = copy.at(2).toInt();
0076             calculateGTwriteData();
0077         }
0078         return true;
0079     }
0080     return false;
0081 }
0082 
0083 void GammaOption::readValue()
0084 {
0085     if (state() == Option::StateHidden) {
0086         return;
0087     }
0088 
0089     QVarLengthArray<unsigned char> data(m_optDesc->size);
0090     SANE_Status status;
0091     SANE_Int res;
0092     status = sane_control_option(m_handle, m_index, SANE_ACTION_GET_VALUE, data.data(), &res);
0093     if (status != SANE_STATUS_GOOD) {
0094         return;
0095     }
0096 
0097     QVector<int> gammaTable;
0098     gammaTable.reserve(data.size() / sizeof(int));
0099     for (int i = 0; i < data.size(); i += sizeof(SANE_Word)) gammaTable.append(toSANE_Word(&data[i]));
0100 
0101     if (gammaTable != m_gammaTable) {
0102         m_gammaTable = gammaTable;
0103 
0104         m_gammaTableMax = m_optDesc->constraint.range->max;
0105 
0106         calculateBCGwriteData();
0107     }
0108 }
0109 
0110 QVariant GammaOption::value() const
0111 {
0112     if (state() == Option::StateHidden) {
0113         return QVariant();
0114     }
0115     return QVariantList{ m_brightness, m_contrast, m_gamma };
0116 }
0117 
0118 int GammaOption::valueSize() const
0119 {
0120     return 3;
0121 }
0122 
0123 QVariant GammaOption::maximumValue() const
0124 {
0125     QVariant value;
0126     if (m_optDesc) {
0127         value = static_cast<float>(m_optDesc->constraint.range->max);
0128         return value;
0129     }
0130     return value;
0131 }
0132 
0133 QString GammaOption::valueAsString() const
0134 {
0135     if (state() == Option::StateHidden) {
0136         return QString();
0137     }
0138 
0139     return QString::asprintf("%d:%d:%d", m_brightness, m_contrast, m_gamma);
0140 }
0141 
0142 void GammaOption::calculateGTwriteData()
0143 {
0144     double maxValue = m_optDesc->constraint.range->max;
0145     double gamma    = 100.0 / m_gamma;
0146     double contrast = (200.0 / (100.0 - m_contrast)) - 1;
0147     double halfMax  = maxValue / 2.0;
0148     // NOTE: This used to add the value times 2, not scaled to maxValue
0149     double brightness = m_brightness * maxValue / 100.0;
0150     double x;
0151 
0152     for (int i = 0; i < m_gammaTable.size(); i++) {
0153         // apply gamma
0154         x = std::pow(static_cast<double>(i) / m_gammaTable.size(), gamma) * maxValue;
0155 
0156         // apply contrast
0157         x = (contrast * (x - halfMax)) + halfMax;
0158 
0159         // apply brightness + rounding
0160         x += brightness + 0.5;
0161 
0162         // ensure correct value
0163         if (x > maxValue) {
0164             x = maxValue;
0165         }
0166         if (x < 0) {
0167             x = 0;
0168         }
0169 
0170         m_gammaTable[i] = static_cast<int>(x);
0171     }
0172 
0173     writeData(m_gammaTable.data());
0174     QVariantList values = { m_brightness, m_contrast, m_gamma };
0175     Q_EMIT valueChanged(values);
0176 }
0177 
0178 void GammaOption::calculateBCGwriteData() {
0179     int beginIndex = 0;
0180     int endIndex = m_gammaTable.size() - 1;
0181     // Find the start and end of the curve, to skip the flat regions
0182     while (beginIndex < endIndex && m_gammaTable[beginIndex] == m_gammaTable[0])
0183         beginIndex++;
0184     while (endIndex > beginIndex && m_gammaTable[endIndex] == m_gammaTable[m_gammaTable.size()-1])
0185         endIndex--;
0186 
0187     float gamma = 0, contrast = 0, brightness = 0;
0188     const QVector<int> &gammaTable = m_gammaTable;
0189     const int &gammaTableMax = m_gammaTableMax;
0190 
0191     auto guessGamma = [&gammaTable, &gamma](int i1, int i2, int step) {
0192         int diff1 = gammaTable[i1 + step] - gammaTable[i1 - step];
0193         int diff2 = gammaTable[i2 + step] - gammaTable[i2 - step];
0194         if (diff1 == 0 || diff2 == 0)
0195             return;
0196         float stepProportion = static_cast<float>(i2) / i1;
0197         float diffProportion = static_cast<float>(diff2) / diff1;
0198         float guessedGamma = log(stepProportion * diffProportion) / log(stepProportion);
0199         gamma += guessedGamma;
0200     };
0201 
0202     auto guessContrast = [&gammaTable, &gammaTableMax, &gamma, &contrast](int i1, int i2, int) {
0203         int prevVal = gammaTable[i1], nextVal = gammaTable[i2];
0204         float scaledDiff = static_cast<float>(nextVal - prevVal) / gammaTableMax;
0205         float scaledPrevIndex = static_cast<float>(i1) / gammaTable.size();
0206         float scaledNextIndex = static_cast<float>(i2) / gammaTable.size();
0207         float guessedContrast = scaledDiff / (pow(scaledNextIndex, gamma) - pow(scaledPrevIndex, gamma));
0208         contrast += guessedContrast;
0209     };
0210 
0211     auto guessBrightness = [&gammaTable, &gammaTableMax, &gamma, &contrast, &brightness](int i, int, int) {
0212         float scaledThisVal = static_cast<float>(gammaTable[i]) / gammaTableMax;
0213         float scaledIndex = static_cast<float>(i) / gammaTable.size();
0214         float guessedBrightness = scaledThisVal - ((pow(scaledIndex, gamma) - 0.5) * contrast + 0.5);
0215         brightness += guessedBrightness;
0216     };
0217 
0218     const int numberOfApproximations = 16;
0219 
0220     auto passValuePairsAndSteps = [&beginIndex, &endIndex](auto func) {
0221         const int step = (endIndex - beginIndex) / 8;
0222         for (int i = 0; i < numberOfApproximations;) {
0223             // Calculate step, even if not passed to the function, to separate the samples
0224             int i1 = rand() % (endIndex - beginIndex - 2 * step - 2) + beginIndex + step + 1;
0225             int i2 = rand() % (endIndex - beginIndex - 2 * step - 2) + beginIndex + step + 1;
0226             if (i2 - i1 >= 4 * step) {
0227                 func(i1, i2, step);
0228                 i++;
0229             }
0230         }
0231     };
0232 
0233     if (endIndex == beginIndex) {
0234         qCDebug(KSANECORE_LOG()) << "Ignoring gamma table: horizontal line at" << m_gammaTable[0];
0235         setValue(QVariantList{0, 0, 100}); // Ignore the table, it's wrong
0236         return;
0237     }
0238 
0239     if (endIndex - beginIndex <= 32) { // Table too small, make single guesses
0240         if (endIndex - beginIndex > 4) { // Measurements don't overlap by just one value
0241             guessGamma(beginIndex + 2, endIndex - 2, 2);
0242         } else {
0243             gamma = 1.0; // Assume linear gamma
0244         }
0245         guessContrast(beginIndex, endIndex, 0);
0246         guessBrightness((beginIndex + endIndex) / 2, 0, 0);
0247     } else {
0248         passValuePairsAndSteps(guessGamma);
0249         gamma /= numberOfApproximations;
0250 
0251         passValuePairsAndSteps(guessContrast);
0252         contrast /= numberOfApproximations;
0253 
0254         passValuePairsAndSteps(guessBrightness);
0255         brightness /= numberOfApproximations;
0256     }
0257 
0258     int newGamma = 100.0 / gamma;
0259     int newContrast = 100.0 - 200.0 / (contrast + 1.0);
0260     int newBrightness = brightness * 100.0;
0261 
0262     if (m_gamma != newGamma || m_contrast != newContrast || m_brightness != newBrightness) {
0263         m_gamma = newGamma;
0264         m_contrast = newContrast;
0265         m_brightness = newBrightness;
0266 
0267         QVariantList values = { m_brightness, m_contrast, m_gamma };
0268         Q_EMIT valueChanged(values);
0269     }
0270 }
0271 
0272 } // namespace KSaneCore
0273 
0274 #include "moc_gammaoption.cpp"