diff --git a/libs/ui/widgets/kis_iconwidget.h b/libs/ui/widgets/kis_iconwidget.h index 3267985a5f..5d9e6e9262 100644 --- a/libs/ui/widgets/kis_iconwidget.h +++ b/libs/ui/widgets/kis_iconwidget.h @@ -1,48 +1,49 @@ /* * Copyright (c) 2000 Matthias Elter * Copyright (c) 2003 Patrick Julien * * 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 KIS_ICONWIDGET_H_ #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 public: KisIconWidget(QWidget *parent = 0, const char *name = 0); void setResource(KoResource * resource); protected: void paintEvent(QPaintEvent *) override; private: KoResource *m_resource; }; #endif // KIS_ICONWIDGET_H_ diff --git a/libs/widgets/KoResourceItemDelegate.cpp b/libs/widgets/KoResourceItemDelegate.cpp index 1bb43a466a..b1826481c2 100644 --- a/libs/widgets/KoResourceItemDelegate.cpp +++ b/libs/widgets/KoResourceItemDelegate.cpp @@ -1,87 +1,94 @@ /* This file is part of the KDE project * Copyright (C) 2008 Jan Hambrecht * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "KoResourceItemDelegate.h" #include +#include #include KoResourceItemDelegate::KoResourceItemDelegate( QObject * parent ) : QAbstractItemDelegate( parent ), m_checkerPainter( 4 ) { } void KoResourceItemDelegate::paint( QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index ) const { if( ! index.isValid() ) return; KoResource * resource = static_cast( index.internalPointer() ); if (!resource) return; painter->save(); if (option.state & QStyle::State_Selected) painter->fillRect( option.rect, option.palette.highlight() ); 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(); QLinearGradient paintGradient; paintGradient.setStops( g->stops() ); paintGradient.setStart( innerRect.topLeft() ); paintGradient.setFinalStop( innerRect.topRight() ); m_checkerPainter.paint( *painter, innerRect ); painter->fillRect( innerRect, QBrush( paintGradient ) ); 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(); QSize imageSize = thumbnail.size(); if(imageSize.height() > innerRect.height() || imageSize.width() > innerRect.width()) { qreal scaleW = static_cast( innerRect.width() ) / static_cast( imageSize.width() ); qreal scaleH = static_cast( innerRect.height() ) / static_cast( imageSize.height() ); qreal scale = qMin( scaleW, scaleH ); int thumbW = static_cast( imageSize.width() * scale ); int thumbH = static_cast( imageSize.height() * scale ); thumbnail = thumbnail.scaled( thumbW, thumbH, Qt::IgnoreAspectRatio, Qt::SmoothTransformation ); } painter->setRenderHint(QPainter::SmoothPixmapTransform, true); if (thumbnail.hasAlphaChannel()) { painter->fillRect(innerRect, Qt::white); // no checkers, they are confusing with patterns. } painter->fillRect( innerRect, QBrush(thumbnail) ); } painter->restore(); } QSize KoResourceItemDelegate::sizeHint( const QStyleOptionViewItem & optionItem, const QModelIndex & ) const { return optionItem.decorationSize; } diff --git a/plugins/filters/CMakeLists.txt b/plugins/filters/CMakeLists.txt index b3db3dcc7c..9da7efe518 100644 --- a/plugins/filters/CMakeLists.txt +++ b/plugins/filters/CMakeLists.txt @@ -1,31 +1,32 @@ add_subdirectory( tests ) add_subdirectory( blur ) add_subdirectory( colors ) add_subdirectory( colorsfilters ) add_subdirectory( convolutionfilters ) add_subdirectory( guassianhighpass) add_subdirectory( embossfilter ) add_subdirectory( example ) add_subdirectory( fastcolortransfer ) add_subdirectory( imageenhancement ) add_subdirectory( noisefilter ) add_subdirectory( oilpaintfilter ) add_subdirectory( pixelizefilter ) add_subdirectory( raindropsfilter ) add_subdirectory( randompickfilter ) add_subdirectory( roundcorners ) add_subdirectory( smalltilesfilter ) add_subdirectory( unsharp ) add_subdirectory( wavefilter ) add_subdirectory( levelfilter ) add_subdirectory( dodgeburn ) add_subdirectory( phongbumpmap ) add_subdirectory( posterize ) add_subdirectory( indexcolors ) add_subdirectory( normalize ) add_subdirectory( gradientmap ) add_subdirectory( threshold ) add_subdirectory( halftone ) 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 index 0000000000..1573abce5b --- /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 index 0000000000..96142193ee --- /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.cpp b/plugins/filters/palettize/palettize.cpp new file mode 100644 index 0000000000..4ef7a14c14 --- /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" diff --git a/plugins/filters/palettize/palettize.h b/plugins/filters/palettize/palettize.h new file mode 100644 index 0000000000..c4db9ab6e5 --- /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