1 /*
2 * This file is part of Krita
3 *
4 * Copyright (c) 2018 Jouni Pentikainen <joupent@gmail.com>
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 */
20
21 #include "kis_multichannel_filter_base.h"
22
23 #include <Qt>
24 #include <QLayout>
25 #include <QPixmap>
26 #include <QPainter>
27 #include <QLabel>
28 #include <QComboBox>
29 #include <QDomDocument>
30 #include <QHBoxLayout>
31 #include <QMessageBox>
32
33 #include "KoChannelInfo.h"
34 #include "KoBasicHistogramProducers.h"
35 #include "KoColorModelStandardIds.h"
36 #include "KoColorSpace.h"
37 #include "KoColorTransformation.h"
38 #include "KoCompositeColorTransformation.h"
39 #include "KoCompositeOp.h"
40 #include "KoID.h"
41
42 #include "kis_signals_blocker.h"
43
44 #include "kis_bookmarked_configuration_manager.h"
45 #include "kis_config_widget.h"
46 #include <filter/kis_filter_category_ids.h>
47 #include <filter/kis_filter_configuration.h>
48 #include <kis_selection.h>
49 #include <kis_paint_device.h>
50 #include <kis_processing_information.h>
51
52 #include "kis_histogram.h"
53 #include "kis_painter.h"
54 #include "widgets/kis_curve_widget.h"
55
KisMultiChannelFilter(const KoID & id,const QString & entry)56 KisMultiChannelFilter::KisMultiChannelFilter(const KoID& id, const QString &entry)
57 : KisColorTransformationFilter(id, FiltersCategoryAdjustId, entry)
58 {
59 setSupportsPainting(true);
60 setColorSpaceIndependence(TO_LAB16);
61 }
62
needsTransparentPixels(const KisFilterConfigurationSP config,const KoColorSpace * cs) const63 bool KisMultiChannelFilter::needsTransparentPixels(const KisFilterConfigurationSP config, const KoColorSpace *cs) const
64 {
65 Q_UNUSED(config);
66 return cs->colorModelId() == AlphaColorModelID;
67 }
68
getVirtualChannels(const KoColorSpace * cs,int maxChannels)69 QVector<VirtualChannelInfo> KisMultiChannelFilter::getVirtualChannels(const KoColorSpace *cs, int maxChannels)
70 {
71 const bool supportsLightness =
72 cs->colorModelId() != LABAColorModelID &&
73 cs->colorModelId() != GrayAColorModelID &&
74 cs->colorModelId() != GrayColorModelID &&
75 cs->colorModelId() != AlphaColorModelID;
76
77 const bool supportsHue = supportsLightness;
78 const bool supportSaturation = supportsLightness;
79
80 QVector<VirtualChannelInfo> vchannels;
81
82 QList<KoChannelInfo *> sortedChannels =
83 KoChannelInfo::displayOrderSorted(cs->channels());
84
85 if (supportsLightness) {
86 vchannels << VirtualChannelInfo(VirtualChannelInfo::ALL_COLORS, -1, 0, cs);
87 }
88
89 Q_FOREACH (KoChannelInfo *channel, sortedChannels) {
90 int pixelIndex = KoChannelInfo::displayPositionToChannelIndex(channel->displayPosition(), sortedChannels);
91 vchannels << VirtualChannelInfo(VirtualChannelInfo::REAL, pixelIndex, channel, cs);
92 }
93
94 if (supportsHue) {
95 vchannels << VirtualChannelInfo(VirtualChannelInfo::HUE, -1, 0, cs);
96 }
97
98 if (supportSaturation) {
99 vchannels << VirtualChannelInfo(VirtualChannelInfo::SATURATION, -1, 0, cs);
100 }
101
102 if (supportsLightness) {
103 vchannels << VirtualChannelInfo(VirtualChannelInfo::LIGHTNESS, -1, 0, cs);
104 }
105
106 if (maxChannels >= 0 && vchannels.size() > maxChannels) {
107 vchannels.resize(maxChannels);
108 }
109
110 return vchannels;
111 }
112
findChannel(const QVector<VirtualChannelInfo> & virtualChannels,const VirtualChannelInfo::Type & channelType)113 int KisMultiChannelFilter::findChannel(const QVector<VirtualChannelInfo> &virtualChannels,
114 const VirtualChannelInfo::Type &channelType)
115 {
116 for (int i = 0; i < virtualChannels.size(); i++) {
117 if (virtualChannels[i].type() == channelType) {
118 return i;
119 }
120 }
121 return -1;
122 }
123
124
KisMultiChannelFilterConfiguration(int channelCount,const QString & name,qint32 version)125 KisMultiChannelFilterConfiguration::KisMultiChannelFilterConfiguration(int channelCount, const QString & name, qint32 version)
126 : KisColorTransformationConfiguration(name, version)
127 , m_channelCount(channelCount)
128 {
129 m_transfers.resize(m_channelCount);
130 }
131
~KisMultiChannelFilterConfiguration()132 KisMultiChannelFilterConfiguration::~KisMultiChannelFilterConfiguration()
133 {}
134
init()135 void KisMultiChannelFilterConfiguration::init()
136 {
137 m_curves.clear();
138 for (int i = 0; i < m_channelCount; ++i) {
139 m_curves.append(getDefaultCurve());
140 }
141 updateTransfers();
142 }
143
isCompatible(const KisPaintDeviceSP dev) const144 bool KisMultiChannelFilterConfiguration::isCompatible(const KisPaintDeviceSP dev) const
145 {
146 return (int)dev->compositionSourceColorSpace()->channelCount() == m_channelCount;
147 }
148
setCurves(QList<KisCubicCurve> & curves)149 void KisMultiChannelFilterConfiguration::setCurves(QList<KisCubicCurve> &curves)
150 {
151 m_curves.clear();
152 m_curves = curves;
153 m_channelCount = curves.size();
154
155 updateTransfers();
156 }
157
updateTransfers()158 void KisMultiChannelFilterConfiguration::updateTransfers()
159 {
160 m_transfers.resize(m_channelCount);
161 for (int i = 0; i < m_channelCount; i++) {
162 m_transfers[i] = m_curves[i].uint16Transfer();
163 }
164 }
165
166 const QVector<QVector<quint16> >&
transfers() const167 KisMultiChannelFilterConfiguration::transfers() const
168 {
169 return m_transfers;
170 }
171
172 const QList<KisCubicCurve>&
curves() const173 KisMultiChannelFilterConfiguration::curves() const
174 {
175 return m_curves;
176 }
177
fromLegacyXML(const QDomElement & root)178 void KisMultiChannelFilterConfiguration::fromLegacyXML(const QDomElement& root)
179 {
180 fromXML(root);
181 }
182
fromXML(const QDomElement & root)183 void KisMultiChannelFilterConfiguration::fromXML(const QDomElement& root)
184 {
185 QList<KisCubicCurve> curves;
186 quint16 numTransfers = 0;
187 int version;
188 version = root.attribute("version").toInt();
189
190 QDomElement e = root.firstChild().toElement();
191 QString attributeName;
192 KisCubicCurve curve;
193 quint16 index;
194 while (!e.isNull()) {
195 if ((attributeName = e.attribute("name")) == "nTransfers") {
196 numTransfers = e.text().toUShort();
197 } else {
198 QRegExp rx("curve(\\d+)");
199
200 if (rx.indexIn(attributeName, 0) != -1) {
201
202 index = rx.cap(1).toUShort();
203 index = qMin(index, quint16(curves.count()));
204
205 if (!e.text().isEmpty()) {
206 curve.fromString(e.text());
207 }
208 curves.insert(index, curve);
209 }
210 }
211 e = e.nextSiblingElement();
212 }
213
214 //prepend empty curves for the brightness contrast filter.
215 if(getString("legacy") == "brightnesscontrast") {
216 if (getString("colorModel") == LABAColorModelID.id()) {
217 curves.append(KisCubicCurve());
218 curves.append(KisCubicCurve());
219 curves.append(KisCubicCurve());
220 } else {
221 int extraChannels = 5;
222 if (getString("colorModel") == CMYKAColorModelID.id()) {
223 extraChannels = 6;
224 } else if (getString("colorModel") == GrayAColorModelID.id()) {
225 extraChannels = 0;
226 }
227 for(int c = 0; c < extraChannels; c ++) {
228 curves.insert(0, KisCubicCurve());
229 }
230 }
231 }
232 if (!numTransfers)
233 return;
234
235 setVersion(version);
236 setCurves(curves);
237 }
238
239 /**
240 * Inherited from KisPropertiesConfiguration
241 */
242 //void KisMultiChannelFilterConfiguration::fromXML(const QString& s)
243
addParamNode(QDomDocument & doc,QDomElement & root,const QString & name,const QString & value)244 void addParamNode(QDomDocument& doc,
245 QDomElement& root,
246 const QString &name,
247 const QString &value)
248 {
249 QDomText text = doc.createTextNode(value);
250 QDomElement t = doc.createElement("param");
251 t.setAttribute("name", name);
252 t.appendChild(text);
253 root.appendChild(t);
254 }
255
toXML(QDomDocument & doc,QDomElement & root) const256 void KisMultiChannelFilterConfiguration::toXML(QDomDocument& doc, QDomElement& root) const
257 {
258 /**
259 * @code
260 * <params version=1>
261 * <param name="nTransfers">3</param>
262 * <param name="curve0">0,0;0.5,0.5;1,1;</param>
263 * <param name="curve1">0,0;1,1;</param>
264 * <param name="curve2">0,0;1,1;</param>
265 * </params>
266 * @endcode
267 */
268
269 root.setAttribute("version", version());
270
271 QDomText text;
272 QDomElement t;
273
274 addParamNode(doc, root, "nTransfers", QString::number(m_channelCount));
275
276 KisCubicCurve curve;
277 QString paramName;
278
279 for (int i = 0; i < m_curves.size(); ++i) {
280 QString name = QLatin1String("curve") + QString::number(i);
281 QString value = m_curves[i].toString();
282
283 addParamNode(doc, root, name, value);
284 }
285 }
286
compareTo(const KisPropertiesConfiguration * rhs) const287 bool KisMultiChannelFilterConfiguration::compareTo(const KisPropertiesConfiguration *rhs) const
288 {
289 const KisMultiChannelFilterConfiguration *otherConfig = dynamic_cast<const KisMultiChannelFilterConfiguration *>(rhs);
290
291 return otherConfig
292 && KisFilterConfiguration::compareTo(rhs)
293 && m_channelCount == otherConfig->m_channelCount
294 && m_curves == otherConfig->m_curves
295 && m_transfers == otherConfig->m_transfers;
296 }
297
KisMultiChannelConfigWidget(QWidget * parent,KisPaintDeviceSP dev,Qt::WindowFlags f)298 KisMultiChannelConfigWidget::KisMultiChannelConfigWidget(QWidget * parent, KisPaintDeviceSP dev, Qt::WindowFlags f)
299 : KisConfigWidget(parent, f)
300 , m_dev(dev)
301 , m_page(new WdgPerChannel(this))
302 {
303 Q_ASSERT(m_dev);
304
305 const KoColorSpace *targetColorSpace = dev->compositionSourceColorSpace();
306 m_virtualChannels = KisMultiChannelFilter::getVirtualChannels(targetColorSpace);
307 }
308
309 /**
310 * Initialize the dialog.
311 * Note: m_virtualChannels must be populated before calling this
312 */
init()313 void KisMultiChannelConfigWidget::init() {
314 QHBoxLayout * layout = new QHBoxLayout(this);
315 Q_CHECK_PTR(layout);
316 layout->setContentsMargins(0,0,0,0);
317 layout->addWidget(m_page);
318
319 resetCurves();
320
321 const int virtualChannelCount = m_virtualChannels.size();
322 for (int i = 0; i < virtualChannelCount; i++) {
323 const VirtualChannelInfo &info = m_virtualChannels[i];
324 m_page->cmbChannel->addItem(info.name(), i);
325 }
326
327 connect(m_page->cmbChannel, SIGNAL(activated(int)), this, SLOT(slotChannelSelected(int)));
328 connect((QObject*)(m_page->chkLogarithmic), SIGNAL(toggled(bool)), this, SLOT(logHistView()));
329 connect((QObject*)(m_page->resetButton), SIGNAL(clicked()), this, SLOT(resetCurve()));
330
331 // create the horizontal and vertical gradient labels
332 m_page->hgradient->setPixmap(createGradient(Qt::Horizontal));
333 m_page->vgradient->setPixmap(createGradient(Qt::Vertical));
334
335 // init histogram calculator
336 const KoColorSpace *targetColorSpace = m_dev->compositionSourceColorSpace();
337 QList<QString> keys =
338 KoHistogramProducerFactoryRegistry::instance()->keysCompatibleWith(targetColorSpace);
339
340 if (keys.size() > 0) {
341 KoHistogramProducerFactory *hpf;
342 hpf = KoHistogramProducerFactoryRegistry::instance()->get(keys.at(0));
343 m_histogram = new KisHistogram(m_dev, m_dev->exactBounds(), hpf->generate(), LINEAR);
344 }
345
346 connect(m_page->curveWidget, SIGNAL(modified()), this, SIGNAL(sigConfigurationItemChanged()));
347
348 {
349 KisSignalsBlocker b(m_page->curveWidget);
350 m_page->curveWidget->setCurve(m_curves[0]);
351 setActiveChannel(0);
352 }
353 }
354
~KisMultiChannelConfigWidget()355 KisMultiChannelConfigWidget::~KisMultiChannelConfigWidget()
356 {
357 delete m_histogram;
358 }
359
resetCurves()360 void KisMultiChannelConfigWidget::resetCurves()
361 {
362 const KisPropertiesConfigurationSP &defaultConfiguration = getDefaultConfiguration();
363 const auto *defaults = dynamic_cast<const KisMultiChannelFilterConfiguration*>(defaultConfiguration.data());
364
365 KIS_SAFE_ASSERT_RECOVER_RETURN(defaults);
366 m_curves = defaults->curves();
367
368 const int virtualChannelCount = m_virtualChannels.size();
369 for (int i = 0; i < virtualChannelCount; i++) {
370 const VirtualChannelInfo &info = m_virtualChannels[i];
371 m_curves[i].setName(info.name());
372 }
373 }
374
setConfiguration(const KisPropertiesConfigurationSP config)375 void KisMultiChannelConfigWidget::setConfiguration(const KisPropertiesConfigurationSP config)
376 {
377 const KisMultiChannelFilterConfiguration * cfg = dynamic_cast<const KisMultiChannelFilterConfiguration *>(config.data());
378 if (!cfg) {
379 return;
380 }
381
382 if (cfg->curves().empty()) {
383 /**
384 * HACK ALERT: our configuration factory generates
385 * default configuration with nTransfers==0.
386 * Catching it here. Just set everything to defaults instead.
387 */
388 const KisPropertiesConfigurationSP &defaultConfiguration = getDefaultConfiguration();
389 const auto *defaults = dynamic_cast<const KisMultiChannelFilterConfiguration*>(defaultConfiguration.data());
390 KIS_SAFE_ASSERT_RECOVER_RETURN(defaults);
391
392 if (!defaults->curves().isEmpty()) {
393 setConfiguration(defaultConfiguration);
394 return;
395 }
396 } else if (cfg->curves().size() > m_virtualChannels.size()) {
397 QMessageBox::warning(this, i18nc("@title:window", "Krita"), i18n("The current configuration was created for a different colorspace and cannot be used. All curves will be reset."));
398 warnKrita << "WARNING: trying to load a curve with invalid number of channels!";
399 warnKrita << "WARNING: expected:" << m_virtualChannels.size();
400 warnKrita << "WARNING: got:" << cfg->curves().size();
401 return;
402 } else {
403 if (cfg->curves().size() < m_virtualChannels.size()) {
404 // The configuration does not cover all our channels.
405 // This happens when loading a document from an older version, which supported fewer channels.
406 // Reset to make sure the unspecified channels have their default values.
407 resetCurves();
408 }
409
410 for (int ch = 0; ch < cfg->curves().size(); ch++) {
411 m_curves[ch] = cfg->curves()[ch];
412 }
413 }
414
415 // HACK: we save the previous curve in setActiveChannel, so just copy it
416 m_page->curveWidget->setCurve(m_curves[m_activeVChannel]);
417
418 setActiveChannel(0);
419 }
420
createGradient(Qt::Orientation orient)421 inline QPixmap KisMultiChannelConfigWidget::createGradient(Qt::Orientation orient /*, int invert (not used yet) */)
422 {
423 int width;
424 int height;
425 int *i, inc, col;
426 int x = 0, y = 0;
427
428 if (orient == Qt::Horizontal) {
429 i = &x; inc = 1; col = 0;
430 width = 256; height = 1;
431 } else {
432 i = &y; inc = -1; col = 255;
433 width = 1; height = 256;
434 }
435
436 QPixmap gradientpix(width, height);
437 QPainter p(&gradientpix);
438 p.setPen(QPen(QColor(0, 0, 0), 1, Qt::SolidLine));
439 for (; *i < 256; (*i)++, col += inc) {
440 p.setPen(QColor(col, col, col));
441 p.drawPoint(x, y);
442 }
443 return gradientpix;
444 }
445
getHistogram()446 inline QPixmap KisMultiChannelConfigWidget::getHistogram()
447 {
448 int i;
449 int height = 256;
450 QPixmap pix(256, height);
451 KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(m_histogram, pix);
452
453
454 bool logarithmic = m_page->chkLogarithmic->isChecked();
455
456 if (logarithmic)
457 m_histogram->setHistogramType(LOGARITHMIC);
458 else
459 m_histogram->setHistogramType(LINEAR);
460
461
462 QPalette appPalette = QApplication::palette();
463
464 pix.fill(QColor(appPalette.color(QPalette::Base)));
465
466 QPainter p(&pix);
467 p.setPen(QColor(appPalette.color(QPalette::Text)));
468 p.save();
469 p.setOpacity(0.2);
470
471 const VirtualChannelInfo &info = m_virtualChannels[m_activeVChannel];
472
473
474 if (info.type() == VirtualChannelInfo::REAL) {
475 m_histogram->setChannel(info.pixelIndex());
476
477 double highest = (double)m_histogram->calculations().getHighest();
478
479 qint32 bins = m_histogram->producer()->numberOfBins();
480
481 if (m_histogram->getHistogramType() == LINEAR) {
482 double factor = (double)height / highest;
483 for (i = 0; i < bins; ++i) {
484 p.drawLine(i, height, i, height - int(m_histogram->getValue(i) * factor));
485 }
486 } else {
487 double factor = (double)height / (double)log(highest);
488 for (i = 0; i < bins; ++i) {
489 p.drawLine(i, height, i, height - int(log((double)m_histogram->getValue(i)) * factor));
490 }
491 }
492 }
493
494 p.restore();
495
496 return pix;
497 }
498
slotChannelSelected(int index)499 void KisMultiChannelConfigWidget::slotChannelSelected(int index)
500 {
501 const int virtualChannel = m_page->cmbChannel->itemData(index).toInt();
502 setActiveChannel(virtualChannel);
503 }
504
setActiveChannel(int ch)505 void KisMultiChannelConfigWidget::setActiveChannel(int ch)
506 {
507 m_curves[m_activeVChannel] = m_page->curveWidget->curve();
508
509 m_activeVChannel = ch;
510 m_page->curveWidget->setCurve(m_curves[m_activeVChannel]);
511 m_page->curveWidget->setPixmap(getHistogram());
512
513 const int index = m_page->cmbChannel->findData(m_activeVChannel);
514 m_page->cmbChannel->setCurrentIndex(index);
515
516 updateChannelControls();
517 }
518
logHistView()519 void KisMultiChannelConfigWidget::logHistView()
520 {
521 m_page->curveWidget->setPixmap(getHistogram());
522 }
523
resetCurve()524 void KisMultiChannelConfigWidget::resetCurve()
525 {
526 const KisPropertiesConfigurationSP &defaultConfiguration = getDefaultConfiguration();
527 const auto *defaults = dynamic_cast<const KisMultiChannelFilterConfiguration*>(defaultConfiguration.data());
528 KIS_SAFE_ASSERT_RECOVER_RETURN(defaults);
529
530 auto defaultCurves = defaults->curves();
531 KIS_SAFE_ASSERT_RECOVER_RETURN(defaultCurves.size() > m_activeVChannel);
532
533 m_page->curveWidget->setCurve(defaultCurves[m_activeVChannel]);
534 }
535