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 }