KSaneCore

gammaoption.cpp
1/*
2 * SPDX-FileCopyrightText: 2009 Kare Sars <kare dot sars at iki dot fi>
3 * SPDX-FileCopyrightText: 2014 Gregor Mitsch : port to KDE5 frameworks
4 *
5 * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
6 */
7
8#include "gammaoption.h"
9
10#include <QVarLengthArray>
11
12#include <ksanecore_debug.h>
13
14#include <cmath>
15
16namespace KSaneCore
17{
18
19GammaOption::GammaOption(const SANE_Handle handle, const int index)
20 : BaseOption(handle, index)
21{
22 m_optionType = Option::TypeGamma;
23}
24
25bool GammaOption::setValue(const QVariant &value)
26{
27 if (state() == Option::StateHidden) {
28 return false;
29 }
30#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
31 if (static_cast<QMetaType::Type>(value.type()) == QMetaType::QString) {
32#else
33 if (value.userType() == QMetaType::QString) {
34#endif
35 const QString stringValue = value.toString();
36 QStringList gammaValues;
37 int brightness;
38 int contrast;
39 int gamma;
40 bool ok = true;
41
42 gammaValues = stringValue.split(QLatin1Char(':'));
43 if (gammaValues.size() != 3) {
44 return false;
45 }
46 brightness = gammaValues.at(0).toInt(&ok);
47 if (ok) {
48 contrast = gammaValues.at(1).toInt(&ok);
49 }
50 if (ok) {
51 gamma = gammaValues.at(2).toInt(&ok);
52 }
53
54 if (ok && (m_brightness != brightness || m_contrast != contrast || m_gamma != gamma) ) {
55 m_brightness = brightness;
56 m_contrast = contrast;
57 m_gamma = gamma;
58 calculateGTwriteData();
59 }
60 return true;
61 }
62 if (value.canConvert<QVariantList>()) { // It's a list
63 QVariantList copy = value.toList();
64#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
65 if (copy.size() != 3 || static_cast<QMetaType::Type>(copy.at(0).type()) != QMetaType::Int
66 || static_cast<QMetaType::Type>(copy.at(1).type()) != QMetaType::Int || static_cast<QMetaType::Type>(copy.at(2).type()) != QMetaType::Int) {
67#else
68 if (copy.size() != 3 || copy.at(0).userType() != QMetaType::Int || copy.at(1).userType() != QMetaType::Int || copy.at(2).userType() != QMetaType::Int) {
69#endif
70 return false;
71 }
72 if (m_brightness != copy.at(0).toInt() || m_contrast != copy.at(1).toInt() || m_gamma != copy.at(2).toInt() ) {
73 m_brightness = copy.at(0).toInt();
74 m_contrast = copy.at(1).toInt();
75 m_gamma = copy.at(2).toInt();
76 calculateGTwriteData();
77 }
78 return true;
79 }
80 return false;
81}
82
83void GammaOption::readValue()
84{
85 if (state() == Option::StateHidden) {
86 return;
87 }
88
89 QVarLengthArray<unsigned char> data(m_optDesc->size);
90 SANE_Status status;
91 SANE_Int res;
92 status = sane_control_option(m_handle, m_index, SANE_ACTION_GET_VALUE, data.data(), &res);
93 if (status != SANE_STATUS_GOOD) {
94 return;
95 }
96
97 QVector<int> gammaTable;
98 gammaTable.reserve(data.size() / sizeof(int));
99 for (int i = 0; i < data.size(); i += sizeof(SANE_Word)) gammaTable.append(toSANE_Word(&data[i]));
100
101 if (gammaTable != m_gammaTable) {
102 m_gammaTable = gammaTable;
103
104 m_gammaTableMax = m_optDesc->constraint.range->max;
105
106 calculateBCGwriteData();
107 }
108}
109
110QVariant GammaOption::value() const
111{
112 if (state() == Option::StateHidden) {
113 return QVariant();
114 }
115 return QVariantList{ m_brightness, m_contrast, m_gamma };
116}
117
118int GammaOption::valueSize() const
119{
120 return 3;
121}
122
123QVariant GammaOption::maximumValue() const
124{
125 QVariant value;
126 if (m_optDesc) {
127 value = static_cast<float>(m_optDesc->constraint.range->max);
128 return value;
129 }
130 return value;
131}
132
133QString GammaOption::valueAsString() const
134{
135 if (state() == Option::StateHidden) {
136 return QString();
137 }
138
139 return QString::asprintf("%d:%d:%d", m_brightness, m_contrast, m_gamma);
140}
141
142void GammaOption::calculateGTwriteData()
143{
144 double maxValue = m_optDesc->constraint.range->max;
145 double gamma = 100.0 / m_gamma;
146 double contrast = (200.0 / (100.0 - m_contrast)) - 1;
147 double halfMax = maxValue / 2.0;
148 // NOTE: This used to add the value times 2, not scaled to maxValue
149 double brightness = m_brightness * maxValue / 100.0;
150 double x;
151
152 for (int i = 0; i < m_gammaTable.size(); i++) {
153 // apply gamma
154 x = std::pow(static_cast<double>(i) / m_gammaTable.size(), gamma) * maxValue;
155
156 // apply contrast
157 x = (contrast * (x - halfMax)) + halfMax;
158
159 // apply brightness + rounding
160 x += brightness + 0.5;
161
162 // ensure correct value
163 if (x > maxValue) {
164 x = maxValue;
165 }
166 if (x < 0) {
167 x = 0;
168 }
169
170 m_gammaTable[i] = static_cast<int>(x);
171 }
172
173 writeData(m_gammaTable.data());
174 QVariantList values = { m_brightness, m_contrast, m_gamma };
175 Q_EMIT valueChanged(values);
176}
177
178void GammaOption::calculateBCGwriteData() {
179 int beginIndex = 0;
180 int endIndex = m_gammaTable.size() - 1;
181 // Find the start and end of the curve, to skip the flat regions
182 while (beginIndex < endIndex && m_gammaTable[beginIndex] == m_gammaTable[0])
183 beginIndex++;
184 while (endIndex > beginIndex && m_gammaTable[endIndex] == m_gammaTable[m_gammaTable.size()-1])
185 endIndex--;
186
187 float gamma = 0, contrast = 0, brightness = 0;
188 const QVector<int> &gammaTable = m_gammaTable;
189 const int &gammaTableMax = m_gammaTableMax;
190
191 auto guessGamma = [&gammaTable, &gamma](int i1, int i2, int step) {
192 int diff1 = gammaTable[i1 + step] - gammaTable[i1 - step];
193 int diff2 = gammaTable[i2 + step] - gammaTable[i2 - step];
194 if (diff1 == 0 || diff2 == 0)
195 return;
196 float stepProportion = static_cast<float>(i2) / i1;
197 float diffProportion = static_cast<float>(diff2) / diff1;
198 float guessedGamma = log(stepProportion * diffProportion) / log(stepProportion);
199 gamma += guessedGamma;
200 };
201
202 auto guessContrast = [&gammaTable, &gammaTableMax, &gamma, &contrast](int i1, int i2, int) {
203 int prevVal = gammaTable[i1], nextVal = gammaTable[i2];
204 float scaledDiff = static_cast<float>(nextVal - prevVal) / gammaTableMax;
205 float scaledPrevIndex = static_cast<float>(i1) / gammaTable.size();
206 float scaledNextIndex = static_cast<float>(i2) / gammaTable.size();
207 float guessedContrast = scaledDiff / (pow(scaledNextIndex, gamma) - pow(scaledPrevIndex, gamma));
208 contrast += guessedContrast;
209 };
210
211 auto guessBrightness = [&gammaTable, &gammaTableMax, &gamma, &contrast, &brightness](int i, int, int) {
212 float scaledThisVal = static_cast<float>(gammaTable[i]) / gammaTableMax;
213 float scaledIndex = static_cast<float>(i) / gammaTable.size();
214 float guessedBrightness = scaledThisVal - ((pow(scaledIndex, gamma) - 0.5) * contrast + 0.5);
215 brightness += guessedBrightness;
216 };
217
218 const int numberOfApproximations = 16;
219
220 auto passValuePairsAndSteps = [&beginIndex, &endIndex](auto func) {
221 const int step = (endIndex - beginIndex) / 8;
222 for (int i = 0; i < numberOfApproximations;) {
223 // Calculate step, even if not passed to the function, to separate the samples
224 int i1 = rand() % (endIndex - beginIndex - 2 * step - 2) + beginIndex + step + 1;
225 int i2 = rand() % (endIndex - beginIndex - 2 * step - 2) + beginIndex + step + 1;
226 if (i2 - i1 >= 4 * step) {
227 func(i1, i2, step);
228 i++;
229 }
230 }
231 };
232
233 if (endIndex == beginIndex) {
234 qCDebug(KSANECORE_LOG()) << "Ignoring gamma table: horizontal line at" << m_gammaTable[0];
235 setValue(QVariantList{0, 0, 100}); // Ignore the table, it's wrong
236 return;
237 }
238
239 if (endIndex - beginIndex <= 32) { // Table too small, make single guesses
240 if (endIndex - beginIndex > 4) { // Measurements don't overlap by just one value
241 guessGamma(beginIndex + 2, endIndex - 2, 2);
242 } else {
243 gamma = 1.0; // Assume linear gamma
244 }
245 guessContrast(beginIndex, endIndex, 0);
246 guessBrightness((beginIndex + endIndex) / 2, 0, 0);
247 } else {
248 passValuePairsAndSteps(guessGamma);
249 gamma /= numberOfApproximations;
250
251 passValuePairsAndSteps(guessContrast);
252 contrast /= numberOfApproximations;
253
254 passValuePairsAndSteps(guessBrightness);
255 brightness /= numberOfApproximations;
256 }
257
258 int newGamma = 100.0 / gamma;
259 int newContrast = 100.0 - 200.0 / (contrast + 1.0);
260 int newBrightness = brightness * 100.0;
261
262 if (m_gamma != newGamma || m_contrast != newContrast || m_brightness != newBrightness) {
263 m_gamma = newGamma;
264 m_contrast = newContrast;
265 m_brightness = newBrightness;
266
267 QVariantList values = { m_brightness, m_contrast, m_gamma };
268 Q_EMIT valueChanged(values);
269 }
270}
271
272} // namespace KSaneCore
273
274#include "moc_gammaoption.cpp"
Q_SCRIPTABLE CaptureState status()
const QList< QKeySequence > & copy()
void append(QList< T > &&value)
const_reference at(qsizetype i) const const
void reserve(qsizetype size)
qsizetype size() const const
QString asprintf(const char *cformat,...)
QStringList split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
Type type() const const
bool canConvert() const const
QList< QVariant > toList() const const
QString toString() const const
int userType() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:19:34 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.