diff --git a/libs/ui/widgets/kis_iconwidget.h b/libs/ui/widgets/kis_iconwidget.h --- a/libs/ui/widgets/kis_iconwidget.h +++ b/libs/ui/widgets/kis_iconwidget.h @@ -21,14 +21,15 @@ #define KIS_ICONWIDGET_H_ #include +#include class KoResource; /** * The icon widget is used in the control box where the current color and brush * are shown. */ -class KisIconWidget : public KisPopupButton +class KRITAUI_EXPORT KisIconWidget : public KisPopupButton { Q_OBJECT diff --git a/libs/widgets/KoResourceItemDelegate.cpp b/libs/widgets/KoResourceItemDelegate.cpp --- a/libs/widgets/KoResourceItemDelegate.cpp +++ b/libs/widgets/KoResourceItemDelegate.cpp @@ -20,6 +20,7 @@ #include "KoResourceItemDelegate.h" #include +#include #include KoResourceItemDelegate::KoResourceItemDelegate( QObject * parent ) @@ -44,6 +45,7 @@ QRect innerRect = option.rect.adjusted( 2, 1, -2, -1 ); KoAbstractGradient * gradient = dynamic_cast( resource ); + KoColorSet * palette = dynamic_cast( resource ); if (gradient) { QGradient * g = gradient->toQGradient(); @@ -57,6 +59,11 @@ delete g; } + else if (palette) { + QImage thumbnail = index.data( Qt::DecorationRole ).value(); + painter->setRenderHint(QPainter::SmoothPixmapTransform, thumbnail.width() > innerRect.width() || thumbnail.height() > innerRect.height()); + painter->drawImage(innerRect, thumbnail); + } else { QImage thumbnail = index.data( Qt::DecorationRole ).value(); diff --git a/plugins/filters/CMakeLists.txt b/plugins/filters/CMakeLists.txt --- a/plugins/filters/CMakeLists.txt +++ b/plugins/filters/CMakeLists.txt @@ -29,3 +29,4 @@ add_subdirectory( edgedetection ) add_subdirectory( convertheightnormalmap ) add_subdirectory( asccdl ) +add_subdirectory( palettize ) diff --git a/plugins/filters/palettize/CMakeLists.txt b/plugins/filters/palettize/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/plugins/filters/palettize/CMakeLists.txt @@ -0,0 +1,4 @@ +set(kritapalettize_SOURCES palettize.cpp) +add_library(kritapalettize MODULE ${kritapalettize_SOURCES}) +target_link_libraries(kritapalettize kritaui) +install(TARGETS kritapalettize DESTINATION ${KRITA_PLUGIN_INSTALL_DIR}) diff --git a/plugins/filters/palettize/kritapalettize.json b/plugins/filters/palettize/kritapalettize.json new file mode 100644 --- /dev/null +++ b/plugins/filters/palettize/kritapalettize.json @@ -0,0 +1,9 @@ +{ + "Id": "Palettize Filter", + "Type": "Service", + "X-KDE-Library": "kritapalettize", + "X-KDE-ServiceTypes": [ + "Krita/Filter" + ], + "X-Krita-Version": "30" +} diff --git a/plugins/filters/palettize/palettize.h b/plugins/filters/palettize/palettize.h new file mode 100644 --- /dev/null +++ b/plugins/filters/palettize/palettize.h @@ -0,0 +1,77 @@ +/* + * This file is part of the KDE project + * + * Copyright (c) 2019 Carl Olsson + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef PALETTIZE_H +#define PALETTIZE_H + +#include +#include +#include +#include +#include +#include +#include + +class KoColorSet; +class KoResourceItemChooser; +class QGroupBox; +class KoPattern; +class QCheckBox; +class QLineEdit; +class QButtonGroup; +class KisDoubleWidget; + +class Palettize : public QObject +{ +public: + Palettize(QObject *parent, const QVariantList &); +}; + +class KisPalettizeWidget : public KisConfigWidget +{ +public: + KisPalettizeWidget(QWidget* parent = 0); + void setConfiguration(const KisPropertiesConfigurationSP) override; + KisPropertiesConfigurationSP configuration() const override; +private: + KoResourceItemChooser* m_paletteWidget; + QGroupBox* m_ditherGroupBox; + QButtonGroup* m_ditherModeGroup; + KoResourceItemChooser* m_ditherPatternWidget; + QCheckBox* m_ditherPatternUseAlphaCheckBox; + QLineEdit* m_ditherNoiseSeedWidget; + KisDoubleWidget* m_ditherWeightWidget; +}; + +class KisFilterPalettize : public KisFilter +{ +public: + enum DitherMode { + Pattern, + Noise + }; + KisFilterPalettize(); + static inline KoID id() { return KoID("palettize", i18n("Palettize")); } + KisConfigWidget* createConfigurationWidget(QWidget* parent, const KisPaintDeviceSP dev, bool useForMasks) const override; + KisFilterConfigurationSP factoryConfiguration() const override; + void processImpl(KisPaintDeviceSP device, const QRect &applyRect, const KisFilterConfigurationSP config, KoUpdater *progressUpdater) const override; +}; + +#endif diff --git a/plugins/filters/palettize/palettize.cpp b/plugins/filters/palettize/palettize.cpp new file mode 100644 --- /dev/null +++ b/plugins/filters/palettize/palettize.cpp @@ -0,0 +1,276 @@ +/* + * This file is part of Krita + * + * Copyright (c) 2019 Carl Olsson + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "palettize.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +K_PLUGIN_FACTORY_WITH_JSON(PalettizeFactory, "kritapalettize.json", registerPlugin();) + +Palettize::Palettize(QObject *parent, const QVariantList &) + : QObject(parent) +{ + KisFilterRegistry::instance()->add(new KisFilterPalettize()); +} + +KisFilterPalettize::KisFilterPalettize() : KisFilter(id(), FiltersCategoryMapId, i18n("&Palettize...")) +{ + setColorSpaceIndependence(FULLY_INDEPENDENT); + setSupportsPainting(true); + setShowConfigurationWidget(true); +} + +KisPalettizeWidget::KisPalettizeWidget(QWidget* parent) + : KisConfigWidget(parent) +{ + QGridLayout* layout = new QGridLayout(this); + layout->setColumnStretch(0, 0); + layout->setColumnStretch(1, 1); + + KisElidedLabel* paletteLabel = new KisElidedLabel(i18n("Palette"), Qt::ElideRight, this); + layout->addWidget(paletteLabel, 0, 0); + KisIconWidget* paletteIcon = new KisIconWidget(this); + paletteIcon->setFixedSize(32, 32); + KoResourceServer* paletteServer = KoResourceServerProvider::instance()->paletteServer(); + QSharedPointer paletteAdapter(new KoResourceServerAdapter(paletteServer)); + m_paletteWidget = new KoResourceItemChooser(paletteAdapter, this, false); + paletteIcon->setPopupWidget(m_paletteWidget); + QObject::connect(m_paletteWidget, &KoResourceItemChooser::resourceSelected, paletteIcon, &KisIconWidget::setResource); + QObject::connect(m_paletteWidget, &KoResourceItemChooser::resourceSelected, this, &KisConfigWidget::sigConfigurationItemChanged); + paletteLabel->setBuddy(paletteIcon); + layout->addWidget(paletteIcon, 0, 1, Qt::AlignLeft); + + m_ditherGroupBox = new QGroupBox(i18n("Dither"), this); + m_ditherGroupBox->setCheckable(true); + QGridLayout* ditherLayout = new QGridLayout(m_ditherGroupBox); + QObject::connect(m_ditherGroupBox, &QGroupBox::toggled, this, &KisConfigWidget::sigConfigurationItemChanged); + ditherLayout->setColumnStretch(0, 0); + ditherLayout->setColumnStretch(1, 1); + layout->addWidget(m_ditherGroupBox, 1, 0, 1, 2); + + QRadioButton* ditherPatternRadio = new QRadioButton(i18n("Pattern"), this); + ditherLayout->addWidget(ditherPatternRadio, 0, 0); + KisIconWidget* ditherPatternIcon = new KisIconWidget(this); + ditherPatternIcon->setFixedSize(32, 32); + KoResourceServer* patternServer = KoResourceServerProvider::instance()->patternServer(); + QSharedPointer patternAdapter(new KoResourceServerAdapter(patternServer)); + m_ditherPatternWidget = new KoResourceItemChooser(patternAdapter, this, false); + ditherPatternIcon->setPopupWidget(m_ditherPatternWidget); + QObject::connect(m_ditherPatternWidget, &KoResourceItemChooser::resourceSelected, ditherPatternIcon, &KisIconWidget::setResource); + QObject::connect(m_ditherPatternWidget, &KoResourceItemChooser::resourceSelected, this, &KisConfigWidget::sigConfigurationItemChanged); + ditherLayout->addWidget(ditherPatternIcon, 0, 1, Qt::AlignLeft); + m_ditherPatternUseAlphaCheckBox = new QCheckBox(i18n("Use alpha"), this); + QObject::connect(m_ditherPatternUseAlphaCheckBox, &QCheckBox::toggled, this, &KisConfigWidget::sigConfigurationItemChanged); + ditherLayout->addWidget(m_ditherPatternUseAlphaCheckBox, 0, 2, Qt::AlignLeft); + + QRadioButton* ditherNoiseRadio = new QRadioButton(i18n("Noise"), this); + ditherLayout->addWidget(ditherNoiseRadio, 1, 0); + m_ditherNoiseSeedWidget = new QLineEdit(this); + m_ditherNoiseSeedWidget->setValidator(new QIntValidator(this)); + QObject::connect(m_ditherNoiseSeedWidget, &QLineEdit::textChanged, this, &KisConfigWidget::sigConfigurationItemChanged); + ditherLayout->addWidget(m_ditherNoiseSeedWidget, 1, 1, 1, 2); + + KisElidedLabel* ditherWeightLabel = new KisElidedLabel(i18n("Weight"), Qt::ElideRight, this); + ditherLayout->addWidget(ditherWeightLabel, 2, 0); + m_ditherWeightWidget = new KisDoubleWidget(this); + m_ditherWeightWidget->setRange(0.0, 1.0); + m_ditherWeightWidget->setSingleStep(0.0625); + m_ditherWeightWidget->setPageStep(0.25); + QObject::connect(m_ditherWeightWidget, &KisDoubleWidget::valueChanged, this, &KisConfigWidget::sigConfigurationItemChanged); + ditherWeightLabel->setBuddy(m_ditherWeightWidget); + ditherLayout->addWidget(m_ditherWeightWidget, 2, 1, 1, 2); + + m_ditherModeGroup = new QButtonGroup(this); + m_ditherModeGroup->addButton(ditherPatternRadio, 0); + m_ditherModeGroup->addButton(ditherNoiseRadio, 1); + QObject::connect(m_ditherModeGroup, QOverload::of(&QButtonGroup::buttonClicked), this, &KisConfigWidget::sigConfigurationItemChanged); + + layout->addItem(new QSpacerItem(0, 0, QSizePolicy::Preferred, QSizePolicy::Expanding), 2, 0, 1, 2); +} + +void KisPalettizeWidget::setConfiguration(const KisPropertiesConfigurationSP config) +{ + KoColorSet* palette = KoResourceServerProvider::instance()->paletteServer()->resourceByName(config->getString("palette")); + if (palette) m_paletteWidget->setCurrentResource(palette); + + m_ditherGroupBox->setChecked(config->getBool("ditherEnabled")); + + QAbstractButton* ditherModeButton = m_ditherModeGroup->button(config->getInt("ditherMode")); + if (ditherModeButton) ditherModeButton->setChecked(true); + + KoPattern* ditherPattern = KoResourceServerProvider::instance()->patternServer()->resourceByName(config->getString("ditherPattern")); + if (ditherPattern) m_ditherPatternWidget->setCurrentResource(ditherPattern); + + m_ditherPatternUseAlphaCheckBox->setChecked(config->getBool("ditherPatternUseAlpha")); + + m_ditherNoiseSeedWidget->setText(QString::number(config->getInt("ditherNoiseSeed"))); + + m_ditherWeightWidget->setValue(config->getDouble("ditherWeight")); +} + +KisPropertiesConfigurationSP KisPalettizeWidget::configuration() const +{ + KisFilterConfigurationSP config = new KisFilterConfiguration("palettize", 1); + if (m_paletteWidget->currentResource()) config->setProperty("palette", QVariant(m_paletteWidget->currentResource()->name())); + config->setProperty("ditherEnabled", m_ditherGroupBox->isChecked()); + config->setProperty("ditherMode", m_ditherModeGroup->checkedId()); + if (m_ditherPatternWidget->currentResource()) config->setProperty("ditherPattern", QVariant(m_ditherPatternWidget->currentResource()->name())); + config->setProperty("ditherPatternUseAlpha", m_ditherPatternUseAlphaCheckBox->isChecked()); + config->setProperty("ditherNoiseSeed", m_ditherNoiseSeedWidget->text().toInt()); + config->setProperty("ditherWeight", m_ditherWeightWidget->value()); + + return config; +} + +KisConfigWidget* KisFilterPalettize::createConfigurationWidget(QWidget *parent, const KisPaintDeviceSP dev, bool useForMasks) const +{ + Q_UNUSED(dev) + Q_UNUSED(useForMasks) + + return new KisPalettizeWidget(parent); +} + +KisFilterConfigurationSP KisFilterPalettize::factoryConfiguration() const +{ + KisFilterConfigurationSP config = new KisFilterConfiguration("palettize", 1); + config->setProperty("palette", "Default"); + config->setProperty("ditherEnabled", false); + config->setProperty("ditherMode", DitherMode::Pattern); + config->setProperty("ditherPattern", "Grid01.pat"); + config->setProperty("ditherPatternUseAlpha", true); + config->setProperty("ditherNoiseSeed", rand()); + config->setProperty("ditherWeight", 1.0); + + return config; +} + +void KisFilterPalettize::processImpl(KisPaintDeviceSP device, const QRect& applyRect, const KisFilterConfigurationSP config, KoUpdater* progressUpdater) const +{ + const KoColorSet* palette = KoResourceServerProvider::instance()->paletteServer()->resourceByName(config->getString("palette")); + const bool ditherEnabled = config->getBool("ditherEnabled"); + const int ditherMode = config->getInt("ditherMode"); + const KoPattern* ditherPattern = KoResourceServerProvider::instance()->patternServer()->resourceByName(config->getString("ditherPattern")); + const bool ditherPatternUseAlpha = config->getBool("ditherPatternUseAlpha"); + const quint64 ditherNoiseSeed = quint64(config->getInt("ditherNoiseSeed")); + const double ditherWeight = config->getDouble("ditherWeight"); + + const KoColorSpace* cs = device->colorSpace(); + KisRandomGenerator random(ditherNoiseSeed); + + using TreeColor = boost::geometry::model::point; + using TreeValue = std::pair>; + using Rtree = boost::geometry::index::rtree>; + Rtree m_rtree; + + if (palette) { + quint16 index = 0; + for (int row = 0; row < palette->rowCount(); ++row) { + for (int column = 0; column < palette->columnCount(); ++column) { + KisSwatch swatch = palette->getColorGlobal(column, row); + if (swatch.isValid()) { + KoColor color = swatch.color().convertedTo(cs); + TreeColor searchColor; + KoColor tempColor; + cs->toLabA16(color.data(), tempColor.data(), 1); + memcpy(reinterpret_cast(&searchColor), tempColor.data(), sizeof(TreeColor)); + // Don't add duplicates so won't dither between identical colors + std::vector result; + m_rtree.query(boost::geometry::index::contains(searchColor), std::back_inserter(result)); + if (result.empty()) m_rtree.insert(std::make_pair(searchColor, std::make_pair(color, index++))); + } + } + } + } + + KisSequentialIteratorProgress it(device, applyRect, progressUpdater); + while (it.nextPixel()) { + // Find 2 nearest palette colors to pixel color + TreeColor imageColor; + KoColor tempColor; + cs->toLabA16(it.oldRawData(), tempColor.data(), 1); + memcpy(reinterpret_cast(&imageColor), tempColor.data(), sizeof(TreeColor)); + std::vector nearestColors; + nearestColors.reserve(2); + for (Rtree::const_query_iterator it = m_rtree.qbegin(boost::geometry::index::nearest(imageColor, 2)); it != m_rtree.qend(); ++it) { + nearestColors.push_back(*it); + } + + if (nearestColors.size() > 0) { + size_t nearestIndex; + // Dither not enabled or only one color found so don't dither + if (!ditherEnabled || nearestColors.size() == 1) nearestIndex = 0; + // Otherwise threshold between colors based on relative distance + else { + std::vector distances(nearestColors.size()); + double distanceSum = 0.0; + for (size_t i = 0; i < nearestColors.size(); ++i) { + distances[i] = boost::geometry::distance(imageColor, nearestColors[i].first); + distanceSum += distances[i]; + } + // Use palette ordering for stable dither color threshold ordering + size_t ditherIndices[2] = {0, 1}; + if (nearestColors[ditherIndices[0]].second.second > nearestColors[ditherIndices[1]].second.second) std::swap(ditherIndices[0], ditherIndices[1]); + const double pos = distances[ditherIndices[0]] / distanceSum; + double threshold = 0.5; + if (ditherMode == DitherMode::Pattern) { + const QImage &image = ditherPattern->pattern(); + const QColor pixel = image.pixelColor(it.x() % image.width(), it.y() % image.height()); + threshold = ditherPatternUseAlpha ? pixel.alphaF() : pixel.lightnessF(); + } + else if (ditherMode == DitherMode::Noise) { + threshold = random.doubleRandomAt(it.x(), it.y()); + } + nearestIndex = pos < (0.5 - (ditherWeight / 2.0) + threshold * ditherWeight) ? ditherIndices[0] : ditherIndices[1]; + } + memcpy(it.rawData(), nearestColors[nearestIndex].second.first.data(), cs->pixelSize()); + } + } +} + +#include "palettize.moc"