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