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"