File indexing completed on 2024-12-22 04:15:24

0001 /*
0002  * This file is part of Krita
0003  *
0004  * SPDX-FileCopyrightText: 2019 Carl Olsson <carl.olsson@gmail.com>
0005  *
0006  *  SPDX-License-Identifier: GPL-2.0-or-later
0007  */
0008 
0009 #include "palettize.h"
0010 
0011 #include <kis_types.h>
0012 #include <kpluginfactory.h>
0013 #include <kis_config_widget.h>
0014 #include <kis_filter_registry.h>
0015 #include <kis_filter_configuration.h>
0016 #include <kis_filter_category_ids.h>
0017 #include <KoUpdater.h>
0018 #include <KisSequentialIteratorProgress.h>
0019 #include <KisResourceItemChooser.h>
0020 #include <KoColorSet.h>
0021 #include <KoPattern.h>
0022 #include <kis_random_generator.h>
0023 #include <KisDitherUtil.h>
0024 #include <KisGlobalResourcesInterface.h>
0025 #include <KoResourceLoadResult.h>
0026 
0027 K_PLUGIN_FACTORY_WITH_JSON(PalettizeFactory, "kritapalettize.json", registerPlugin<Palettize>();)
0028 
0029 Palettize::Palettize(QObject *parent, const QVariantList &)
0030     : QObject(parent)
0031 {
0032     KisFilterRegistry::instance()->add(new KisFilterPalettize());
0033 }
0034 
0035 #include "palettize.moc"
0036 
0037 
0038 /*******************************************************************************/
0039 /*                      KisFilterPalettizeConfiguration                        */
0040 /*******************************************************************************/
0041 
0042 class KisFilterPalettizeConfiguration : public KisFilterConfiguration
0043 {
0044 public:
0045     KisFilterPalettizeConfiguration(const QString & name, qint32 version, KisResourcesInterfaceSP resourcesInterface)
0046         : KisFilterConfiguration(name, version, resourcesInterface)
0047     {
0048     }
0049 
0050     KisFilterPalettizeConfiguration(const KisFilterPalettizeConfiguration &rhs)
0051         : KisFilterConfiguration(rhs)
0052     {
0053     }
0054 
0055     virtual KisFilterConfigurationSP clone() const override {
0056         return new KisFilterPalettizeConfiguration(*this);
0057     }
0058 
0059     KoResourceLoadResult palette(KisResourcesInterfaceSP resourcesInterface) const
0060     {
0061         auto source = resourcesInterface->source<KoColorSet>(ResourceType::Palettes);
0062         const QString md5sum = this->getString("md5sum");
0063         const QString name = this->getString("palette");
0064 
0065         return source.bestMatchLoadResult(md5sum, "", name) ;
0066     }
0067 
0068     KoColorSetSP palette() const
0069     {
0070         return palette(resourcesInterface()).resource<KoColorSet>();
0071     }
0072 
0073     QList<KoResourceLoadResult> linkedResources(KisResourcesInterfaceSP globalResourcesInterface) const override
0074     {
0075 
0076         QList<KoResourceLoadResult> resources;
0077         resources << this->palette(globalResourcesInterface);
0078 
0079         resources << KisDitherWidget::prepareLinkedResources(*this, "dither/", globalResourcesInterface);
0080         resources << KisDitherWidget::prepareLinkedResources(*this, "alphaDither/", globalResourcesInterface);
0081 
0082         return resources;
0083     }
0084 };
0085 
0086 /*******************************************************************************/
0087 /*                      KisPalettizeWidget                                     */
0088 /*******************************************************************************/
0089 
0090 KisPalettizeWidget::KisPalettizeWidget(QWidget* parent)
0091     : KisConfigWidget(parent)
0092 {
0093     Q_UNUSED(m_ditherPatternWidget);
0094     setupUi(this);
0095 
0096     paletteIconWidget->setFixedSize(32, 32);
0097     m_paletteWidget = new KisResourceItemChooser(ResourceType::Palettes, false, this);
0098     paletteIconWidget->setPopupWidget(m_paletteWidget);
0099     QObject::connect(m_paletteWidget, &KisResourceItemChooser::resourceSelected, paletteIconWidget, &KisIconWidget::setResource);
0100     QObject::connect(m_paletteWidget, &KisResourceItemChooser::resourceSelected, this, &KisConfigWidget::sigConfigurationItemChanged);
0101 
0102     QObject::connect(colorspaceComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &KisConfigWidget::sigConfigurationItemChanged);
0103 
0104     QObject::connect(ditherGroupBox, &QGroupBox::toggled, this, &KisConfigWidget::sigConfigurationItemChanged);
0105 
0106     QObject::connect(ditherWidget, &KisDitherWidget::sigConfigurationItemChanged, this, &KisConfigWidget::sigConfigurationItemChanged);
0107 
0108     QObject::connect(colorModeComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &KisConfigWidget::sigConfigurationItemChanged);
0109 
0110     offsetScaleSpinBox->setPrefix(QString("%1  ").arg(i18n("Offset Scale:")));
0111     offsetScaleSpinBox->setRange(0.0, 1.0, 3);
0112     offsetScaleSpinBox->setSingleStep(0.125);
0113     QObject::connect(offsetScaleSpinBox, QOverload<double>::of(&KisDoubleSliderSpinBox::valueChanged), this, &KisConfigWidget::sigConfigurationItemChanged);
0114 
0115     QObject::connect(alphaGroupBox, &QGroupBox::toggled, this, &KisConfigWidget::sigConfigurationItemChanged);
0116 
0117     QObject::connect(alphaModeComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &KisConfigWidget::sigConfigurationItemChanged);
0118 
0119     alphaClipSpinBox->setPrefix(QString("%1  ").arg(i18n("Clip:")));
0120     alphaClipSpinBox->setRange(0.0, 1.0, 3);
0121     alphaClipSpinBox->setSingleStep(0.125);
0122     QObject::connect(alphaClipSpinBox, QOverload<double>::of(&KisDoubleSliderSpinBox::valueChanged), this, &KisConfigWidget::sigConfigurationItemChanged);
0123 
0124     alphaIndexSpinBox->setPrefix(QString("%1  ").arg(i18nc("Index as in Index Color", "Index:")));
0125     alphaIndexSpinBox->setRange(0, 255);
0126     QObject::connect(alphaIndexSpinBox, QOverload<int>::of(&KisSliderSpinBox::valueChanged), this, &KisConfigWidget::sigConfigurationItemChanged);
0127     QObject::connect(m_paletteWidget, &KisResourceItemChooser::resourceSelected, [this](){
0128         const KoColorSetSP palette = m_paletteWidget->currentResource().staticCast<KoColorSet>();
0129         alphaIndexSpinBox->setMaximum(palette ? int(palette->colorCount() - 1) : 0);
0130         alphaIndexSpinBox->setValue(std::min(alphaIndexSpinBox->value(), alphaIndexSpinBox->maximum()));
0131     });
0132 
0133     QObject::connect(alphaDitherWidget, &KisDitherWidget::sigConfigurationItemChanged, this, &KisConfigWidget::sigConfigurationItemChanged);
0134 }
0135 
0136 void KisPalettizeWidget::setConfiguration(const KisPropertiesConfigurationSP _config)
0137 {
0138     const KisFilterPalettizeConfiguration *config = dynamic_cast<const KisFilterPalettizeConfiguration*>(_config.data());
0139     KIS_SAFE_ASSERT_RECOVER_RETURN(config);
0140 
0141     KoColorSetSP palette = config->palette();
0142     if (palette) m_paletteWidget->setCurrentResource(palette);
0143     colorspaceComboBox->setCurrentIndex(config->getInt("colorspace"));
0144     ditherGroupBox->setChecked(config->getBool("ditherEnabled"));
0145     ditherWidget->setConfiguration(*config, "dither/");
0146     colorModeComboBox->setCurrentIndex(config->getInt("dither/colorMode"));
0147     offsetScaleSpinBox->setValue(config->getDouble("dither/offsetScale"));
0148     alphaGroupBox->setChecked(config->getBool("alphaEnabled"));
0149     alphaModeComboBox->setCurrentIndex(config->getInt("alphaMode"));
0150     alphaClipSpinBox->setValue(config->getDouble("alphaClip"));
0151     alphaIndexSpinBox->setValue(config->getInt("alphaIndex"));
0152     alphaDitherWidget->setConfiguration(*config, "alphaDither/");
0153 }
0154 
0155 KisPropertiesConfigurationSP KisPalettizeWidget::configuration() const
0156 {
0157     KisFilterSP filter = KisFilterRegistry::instance()->get("palettize");
0158     KisFilterConfigurationSP config = filter->factoryConfiguration(KisGlobalResourcesInterface::instance());
0159 
0160     if (m_paletteWidget->currentResource()) {
0161         config->setProperty("md5sum", QVariant(m_paletteWidget->currentResource()->md5Sum()));
0162         config->setProperty("palette", QVariant(m_paletteWidget->currentResource()->name()));
0163     }
0164     config->setProperty("colorspace", colorspaceComboBox->currentIndex());
0165     config->setProperty("ditherEnabled", ditherGroupBox->isChecked());
0166     ditherWidget->configuration(*config, "dither/");
0167     config->setProperty("dither/colorMode", colorModeComboBox->currentIndex());
0168     config->setProperty("dither/offsetScale", offsetScaleSpinBox->value());
0169     config->setProperty("alphaEnabled", alphaGroupBox->isChecked());
0170     config->setProperty("alphaMode", alphaModeComboBox->currentIndex());
0171     config->setProperty("alphaClip", alphaClipSpinBox->value());
0172     config->setProperty("alphaIndex", alphaIndexSpinBox->value());
0173     alphaDitherWidget->configuration(*config, "alphaDither/");
0174 
0175     return config;
0176 }
0177 
0178 KisConfigWidget* KisFilterPalettize::createConfigurationWidget(QWidget *parent, const KisPaintDeviceSP dev, bool useForMasks) const
0179 {
0180     Q_UNUSED(dev);
0181     Q_UNUSED(useForMasks);
0182 
0183     return new KisPalettizeWidget(parent);
0184 }
0185 
0186 /*******************************************************************************/
0187 /*                      KisFilterPalettize                                     */
0188 /*******************************************************************************/
0189 
0190 KisFilterPalettize::KisFilterPalettize() : KisFilter(id(), FiltersCategoryMapId, i18n("&Palettize..."))
0191 {
0192     setColorSpaceIndependence(FULLY_INDEPENDENT);
0193     setSupportsPainting(true);
0194     setShowConfigurationWidget(true);
0195 }
0196 
0197 KisFilterConfigurationSP KisFilterPalettize::factoryConfiguration(KisResourcesInterfaceSP resourcesInterface) const
0198 {
0199     return new KisFilterPalettizeConfiguration("palettize", 1, resourcesInterface);
0200 }
0201 
0202 
0203 KisFilterConfigurationSP KisFilterPalettize::defaultConfiguration(KisResourcesInterfaceSP resourcesInterface) const
0204 {
0205     KisFilterConfigurationSP config = factoryConfiguration(resourcesInterface);
0206 
0207     config->setProperty("palette", "Default");
0208     config->setProperty("colorspace", Colorspace::Lab);
0209     config->setProperty("ditherEnabled", false);
0210     KisDitherWidget::factoryConfiguration(*config, "dither/");
0211     config->setProperty("dither/colorMode", ColorMode::PerChannelOffset);
0212     config->setProperty("dither/offsetScale", 0.125);
0213     config->setProperty("alphaEnabled", true);
0214     config->setProperty("alphaMode", AlphaMode::Clip);
0215     config->setProperty("alphaClip", 0.5);
0216     config->setProperty("alphaIndex", 0);
0217     KisDitherWidget::factoryConfiguration(*config, "alphaDither/");
0218 
0219     return config;
0220 }
0221 
0222 void KisFilterPalettize::processImpl(KisPaintDeviceSP device, const QRect& applyRect, const KisFilterConfigurationSP _config, KoUpdater* progressUpdater) const
0223 {
0224     const KisFilterPalettizeConfiguration *config = dynamic_cast<const KisFilterPalettizeConfiguration*>(_config.data());
0225     KIS_SAFE_ASSERT_RECOVER_RETURN(config);
0226     KIS_SAFE_ASSERT_RECOVER_NOOP(config->hasLocalResourcesSnapshot());
0227 
0228     const KoColorSetSP palette = config->palette();
0229 
0230     const int searchColorspace = config->getInt("colorspace");
0231     const bool ditherEnabled = config->getBool("ditherEnabled");
0232     const int colorMode = config->getInt("dither/colorMode");
0233     const double offsetScale = config->getDouble("dither/offsetScale");
0234     const bool alphaEnabled = config->getBool("alphaEnabled");
0235     const int alphaMode = config->getInt("alphaMode");
0236     const double alphaClip = config->getDouble("alphaClip");
0237     const int alphaIndex = config->getInt("alphaIndex");
0238 
0239     const KoColorSpace* colorspace = device->colorSpace();
0240     const KoColorSpace* workColorspace = (searchColorspace == Colorspace::Lab
0241                                               ? KoColorSpaceRegistry::instance()->lab16()
0242                                               : KoColorSpaceRegistry::instance()->rgb16("sRGB-elle-V2-srgbtrc.icc"));
0243 
0244     const quint8 colorCount = ditherEnabled && colorMode == ColorMode::NearestColors ? 2 : 1;
0245 
0246     using SearchColor = boost::geometry::model::point<quint16, 3, boost::geometry::cs::cartesian>;
0247     struct ColorCandidate {
0248         KoColor color;
0249         quint16 index;
0250         double distance;
0251     };
0252     using SearchEntry = std::pair<SearchColor, ColorCandidate>;
0253     boost::geometry::index::rtree<SearchEntry, boost::geometry::index::quadratic<16>> rtree;
0254 
0255     if (palette) {
0256         // Add palette colors to search tree
0257         quint16 index = 0;
0258         for (int row = 0; row < palette->rowCount(); ++row) {
0259             for (int column = 0; column < palette->columnCount(); ++column) {
0260                 KisSwatch swatch = palette->getColorGlobal(column, row);
0261                 if (swatch.isValid()) {
0262                     KoColor color = swatch.color().convertedTo(colorspace);
0263                     KoColor workColor = swatch.color().convertedTo(workColorspace);
0264                     SearchColor searchColor;
0265                     memcpy(&searchColor, workColor.data(), sizeof(SearchColor));
0266                     // Don't add duplicates so won't dither between identical colors
0267                     std::vector<SearchEntry> result;
0268                     rtree.query(boost::geometry::index::contains(searchColor), std::back_inserter(result));
0269                     if (result.empty()) rtree.insert(SearchEntry(searchColor, {color, index, 0.0}));
0270                 }
0271                 ++index;
0272             }
0273         }
0274 
0275         KisDitherUtil ditherUtil;
0276         if (ditherEnabled) ditherUtil.setConfiguration(*config, "dither/");
0277 
0278         KisDitherUtil alphaDitherUtil;
0279         if (alphaMode == AlphaMode::Dither) alphaDitherUtil.setConfiguration(*config, "alphaDither/");
0280 
0281         KisSequentialIteratorProgress pixel(device, applyRect, progressUpdater);
0282         while (pixel.nextPixel()) {
0283             KoColor workColor(pixel.oldRawData(), colorspace);
0284             workColor.convertTo(workColorspace);
0285 
0286             // Find dither threshold
0287             double threshold = 0.5;
0288             if (ditherEnabled) {
0289                 threshold = ditherUtil.threshold(QPoint(pixel.x(), pixel.y()));
0290 
0291                 // Traditional per-channel ordered dithering
0292                 if (colorMode == ColorMode::PerChannelOffset) {
0293                     QVector<float> normalized(int(workColorspace->channelCount()));
0294                     workColorspace->normalisedChannelsValue(workColor.data(), normalized);
0295                     for (int channel = 0; channel < int(workColorspace->channelCount()); ++channel) {
0296                         normalized[channel] += (threshold - 0.5) * offsetScale;
0297                     }
0298                     workColorspace->fromNormalisedChannelsValue(workColor.data(), normalized);
0299                 }
0300             }
0301 
0302             // Get candidate colors and their distances
0303             SearchColor searchColor;
0304             memcpy(reinterpret_cast<quint8 *>(&searchColor), workColor.data(), sizeof(SearchColor));
0305             std::vector<ColorCandidate> candidateColors;
0306             candidateColors.reserve(size_t(colorCount));
0307             double distanceSum = 0.0;
0308             for (auto it = rtree.qbegin(boost::geometry::index::nearest(searchColor, colorCount)); it != rtree.qend() && candidateColors.size() < colorCount; ++it) {
0309                 ColorCandidate candidate = it->second;
0310                 candidate.distance = boost::geometry::distance(searchColor, it->first);
0311                 candidateColors.push_back(candidate);
0312                 distanceSum += candidate.distance;
0313             }
0314 
0315             // Select color candidate
0316             quint16 selected;
0317             if (ditherEnabled && colorMode == ColorMode::NearestColors) {
0318                 // Sort candidates by palette order for stable dither color ordering
0319                 const bool swap = candidateColors[0].index > candidateColors[1].index;
0320                 selected = swap ^ (candidateColors[swap].distance / distanceSum > threshold);
0321             }
0322             else {
0323                 selected = 0;
0324             }
0325             ColorCandidate &candidate = candidateColors[selected];
0326 
0327             // Set alpha
0328             const double oldAlpha = colorspace->opacityF(pixel.oldRawData());
0329             double newAlpha = oldAlpha;
0330             if (alphaEnabled && !(!ditherEnabled && alphaMode == AlphaMode::Dither)) {
0331                 if (alphaMode == AlphaMode::Clip) {
0332                     newAlpha = oldAlpha < alphaClip? 0.0 : 1.0;
0333                 }
0334                 else if (alphaMode == AlphaMode::Index) {
0335                     newAlpha = (candidate.index == alphaIndex ? 0.0 : 1.0);
0336                 }
0337                 else if (alphaMode == AlphaMode::Dither) {
0338                     newAlpha = oldAlpha < alphaDitherUtil.threshold(QPoint(pixel.x(), pixel.y())) ? 0.0 : 1.0;
0339                 }
0340             }
0341             colorspace->setOpacity(candidate.color.data(), newAlpha, 1);
0342 
0343             // Copy color to pixel
0344             memcpy(pixel.rawData(), candidate.color.data(), colorspace->pixelSize());
0345         }
0346     }
0347 }