diff --git a/libs/image/kis_paint_device.cc b/libs/image/kis_paint_device.cc --- a/libs/image/kis_paint_device.cc +++ b/libs/image/kis_paint_device.cc @@ -1443,7 +1443,7 @@ void KisPaintDevice::fill(const QRect & rc, const KoColor &color) { - Q_ASSERT(*color.colorSpace() == *colorSpace()); + KIS_ASSERT_RECOVER_RETURN(*color.colorSpace() == *colorSpace()); m_d->currentStrategy()->fill(rc, color.data()); } diff --git a/libs/pigment/KoColor.h b/libs/pigment/KoColor.h --- a/libs/pigment/KoColor.h +++ b/libs/pigment/KoColor.h @@ -99,13 +99,25 @@ const KoColorProfile *profile() const; /// Convert this KoColor to the specified colorspace. If the specified colorspace is the - /// same as the original colorspace, do nothing. Returns the converted KoColor. + /// same as the original colorspace, do nothing void convertTo(const KoColorSpace * cs, KoColorConversionTransformation::Intent renderingIntent, KoColorConversionTransformation::ConversionFlags conversionFlags); void convertTo(const KoColorSpace * cs); + /// Copies this color and converts it to the specified colorspace. If the specified colorspace is the + /// same as the original colorspace, just returns a copy + KoColor convertedTo(const KoColorSpace * cs, + KoColorConversionTransformation::Intent renderingIntent, + KoColorConversionTransformation::ConversionFlags conversionFlags) const; + + /// Copies this color and converts it to the specified colorspace. If the specified colorspace is the + /// same as the original colorspace, just returns a copy + KoColor convertedTo(const KoColorSpace * cs) const; + + + /// assign new profile without converting pixel data void setProfile(const KoColorProfile *profile); @@ -154,6 +166,35 @@ return m_data; } + + /** + * Channelwise subtracts \p value from *this and stores the result in *this + * + * Throws a safe assert if the colorspaces of the two colors are different + */ + void subtract(const KoColor &value); + + /** + * Channelwise subtracts \p value from a copy of *this and returns the result + * + * Throws a safe assert if the colorspaces of the two colors are different + */ + KoColor subtracted(const KoColor &value) const; + + /** + * Channelwise adds \p value to *this and stores the result in *this + * + * Throws a safe assert if the colorspaces of the two colors are different + */ + void add(const KoColor &value); + + /** + * Channelwise adds \p value to a copy of *this and returns the result + * + * Throws a safe assert if the colorspaces of the two colors are different + */ + KoColor added(const KoColor &value) const; + /** * Serialize this color following Create's swatch color specification available * at http://create.freedesktop.org/wiki/index.php/Swatches_-_colour_file_format @@ -225,4 +266,7 @@ Q_DECLARE_METATYPE(KoColor) +KRITAPIGMENT_EXPORT QDebug operator<<(QDebug dbg, const KoColor &color); + + #endif diff --git a/libs/pigment/KoColor.cpp b/libs/pigment/KoColor.cpp --- a/libs/pigment/KoColor.cpp +++ b/libs/pigment/KoColor.cpp @@ -135,6 +135,20 @@ KoColorConversionTransformation::internalConversionFlags()); } +KoColor KoColor::convertedTo(const KoColorSpace *cs, KoColorConversionTransformation::Intent renderingIntent, KoColorConversionTransformation::ConversionFlags conversionFlags) const +{ + KoColor result(*this); + result.convertTo(cs, renderingIntent, conversionFlags); + return result; +} + +KoColor KoColor::convertedTo(const KoColorSpace *cs) const +{ + return convertedTo(cs, + KoColorConversionTransformation::internalRenderingIntent(), + KoColorConversionTransformation::internalConversionFlags()); +} + void KoColor::setProfile(const KoColorProfile *profile) { const KoColorSpace *dstColorSpace = @@ -178,6 +192,54 @@ } } +void KoColor::subtract(const KoColor &value) +{ + KIS_SAFE_ASSERT_RECOVER_RETURN(*m_colorSpace == *value.colorSpace()); + + QVector channels1(m_colorSpace->channelCount()); + QVector channels2(m_colorSpace->channelCount()); + + m_colorSpace->normalisedChannelsValue(m_data, channels1); + m_colorSpace->normalisedChannelsValue(value.data(), channels2); + + for (int i = 0; i < channels1.size(); i++) { + channels1[i] -= channels2[i]; + } + + m_colorSpace->fromNormalisedChannelsValue(m_data, channels1); +} + +KoColor KoColor::subtracted(const KoColor &value) const +{ + KoColor result(*this); + result.subtract(value); + return result; +} + +void KoColor::add(const KoColor &value) +{ + KIS_SAFE_ASSERT_RECOVER_RETURN(*m_colorSpace == *value.colorSpace()); + + QVector channels1(m_colorSpace->channelCount()); + QVector channels2(m_colorSpace->channelCount()); + + m_colorSpace->normalisedChannelsValue(m_data, channels1); + m_colorSpace->normalisedChannelsValue(value.data(), channels2); + + for (int i = 0; i < channels1.size(); i++) { + channels1[i] += channels2[i]; + } + + m_colorSpace->fromNormalisedChannelsValue(m_data, channels1); +} + +KoColor KoColor::added(const KoColor &value) const +{ + KoColor result(*this); + result.add(value); + return result; +} + #ifndef NDEBUG void KoColor::dump() const { @@ -296,3 +358,58 @@ } return ls.join(" "); } + +QDebug operator<<(QDebug dbg, const KoColor &color) +{ + dbg.nospace() << "KoColor (" << color.colorSpace()->id(); + + QList channels = color.colorSpace()->channels(); + for (auto it = channels.constBegin(); it != channels.constEnd(); ++it) { + + KoChannelInfo *ch = (*it); + + dbg.nospace() << ", " << ch->name() << ":"; + + switch (ch->channelValueType()) { + case KoChannelInfo::UINT8: { + const quint8 *ptr = reinterpret_cast(color.data() + ch->pos()); + dbg.nospace() << *ptr; + break; + } case KoChannelInfo::UINT16: { + const quint16 *ptr = reinterpret_cast(color.data() + ch->pos()); + dbg.nospace() << *ptr; + break; + } case KoChannelInfo::UINT32: { + const quint32 *ptr = reinterpret_cast(color.data() + ch->pos()); + dbg.nospace() << *ptr; + break; + } case KoChannelInfo::FLOAT16: { + const half *ptr = reinterpret_cast(color.data() + ch->pos()); + dbg.nospace() << *ptr; + break; + } case KoChannelInfo::FLOAT32: { + const float *ptr = reinterpret_cast(color.data() + ch->pos()); + dbg.nospace() << *ptr; + break; + } case KoChannelInfo::FLOAT64: { + const double *ptr = reinterpret_cast(color.data() + ch->pos()); + dbg.nospace() << *ptr; + break; + } case KoChannelInfo::INT8: { + const qint8 *ptr = reinterpret_cast(color.data() + ch->pos()); + dbg.nospace() << *ptr; + break; + } case KoChannelInfo::INT16: { + const qint16 *ptr = reinterpret_cast(color.data() + ch->pos()); + dbg.nospace() << *ptr; + break; + } case KoChannelInfo::OTHER: { + const quint8 *ptr = reinterpret_cast(color.data() + ch->pos()); + dbg.nospace() << "undef(" << *ptr << ")"; + break; + } + } + } + dbg.nospace() << ")"; + return dbg.space(); +} diff --git a/plugins/paintops/colorsmudge/kis_colorsmudgeop.h b/plugins/paintops/colorsmudge/kis_colorsmudgeop.h --- a/plugins/paintops/colorsmudge/kis_colorsmudgeop.h +++ b/plugins/paintops/colorsmudge/kis_colorsmudgeop.h @@ -39,6 +39,7 @@ class KoAbstractGradient; class KisBrushBasedPaintOpSettings; class KisPainter; +class KoColorSpace; class KisColorSmudgeOp: public KisBrushBasedPaintOp { @@ -78,6 +79,12 @@ QRect m_dstDabRect; KisFixedPaintDeviceSP m_maskDab; QPointF m_lastPaintPos; + + + bool m_useRoundingCorrection = false; + KoColor m_roundedColorError; + const KoColorSpace *m_preciseColorSpace = 0; + const KoCompositeOp *m_preciseColorRateCompositeOp = 0; }; #endif // _KIS_COLORSMUDGEOP_H_ diff --git a/plugins/paintops/colorsmudge/kis_colorsmudgeop.cpp b/plugins/paintops/colorsmudge/kis_colorsmudgeop.cpp --- a/plugins/paintops/colorsmudge/kis_colorsmudgeop.cpp +++ b/plugins/paintops/colorsmudge/kis_colorsmudgeop.cpp @@ -38,6 +38,7 @@ #include #include #include +#include KisColorSmudgeOp::KisColorSmudgeOp(const KisPaintOpSettingsSP settings, KisPainter* painter, KisNodeSP node, KisImageSP image) @@ -85,6 +86,39 @@ m_colorRatePainter->setCompositeOp(painter->compositeOp()->id()); m_rotationOption.applyFanCornersInfo(this); + + /** + * Initialize rounding correction stuff. If we are painting in 8-bit mode, + * we should keep a special high-precision correction offset to make + * color accumulation work correctly. + * + * When we paint with color rate option, we paint with extremely small + * opacity value. If the paint color has small values in some channels, + * then these channels will not paint anything, while others will still + * add some color value. It will result in massive hue fluctuations, + * irritating the painters. + * + * To overcome this issue we do all the color rate calculations in the + * a separate high precision color space (float32) and round it up only + * right before painting on canvas. Then we save the rounding error in + * m_roundedColorError and use it to correct the fill color of the nect dab. + */ + const KoColorSpace *baseSpace = m_tempDev->colorSpace(); + m_useRoundingCorrection = baseSpace->colorDepthId() == Integer8BitsColorDepthID; + + if (m_useRoundingCorrection) { + // we need a floating point color space to handle negative values properly + m_preciseColorSpace = + KoColorSpaceRegistry::instance()->colorSpace( + baseSpace->colorModelId().id(), + Float32BitsColorDepthID.id(), + baseSpace->profile()); + } else { + m_preciseColorSpace = baseSpace; + } + + m_preciseColorRateCompositeOp = m_preciseColorSpace->compositeOp(painter->compositeOp()->id()); + m_roundedColorError = KoColor(Qt::transparent, m_preciseColorSpace); } KisColorSmudgeOp::~KisColorSmudgeOp() @@ -215,7 +249,12 @@ m_tempDev->clear(QRect(QPoint(), m_dstDabRect.size())); } - if (m_smudgeRateOption.getMode() == KisSmudgeOption::SMEARING_MODE) { + const bool useDullingMode = m_smudgeRateOption.getMode() == KisSmudgeOption::DULLING_MODE; + + // stored in the color space of the paintColor + KoColor dullingFillColor; + + if (!useDullingMode) { m_smudgePainter->bitBlt(QPoint(), painter()->device(), srcDabRect); } else { QPoint pt = (srcDabRect.topLeft() + hotSpot).toPoint(); @@ -225,7 +264,7 @@ m_smudgeRadiusOption.apply(*m_smudgePainter, info, effectiveSize, pt.x(), pt.y(), painter()->device()); KoColor color2 = m_smudgePainter->paintColor(); - m_smudgePainter->fill(0, 0, m_dstDabRect.width(), m_dstDabRect.height(), color2); + dullingFillColor = color2.convertedTo(m_preciseColorSpace); } else { KoColor color = painter()->paintColor(); @@ -236,7 +275,7 @@ KisCrossDeviceColorPickerInt colorPicker(painter()->device(), color); colorPicker.pickColor(pt.x(), pt.y(), color.data()); - m_smudgePainter->fill(0, 0, m_dstDabRect.width(), m_dstDabRect.height(), color); + dullingFillColor = color.convertedTo(m_preciseColorSpace); } } @@ -254,7 +293,38 @@ // composite mode KoColor color = painter()->paintColor(); m_gradientOption.apply(color, m_gradient, info); - m_colorRatePainter->fill(0, 0, m_dstDabRect.width(), m_dstDabRect.height(), color); + + if (!useDullingMode) { + m_colorRatePainter->fill(0, 0, m_dstDabRect.width(), m_dstDabRect.height(), color); + } else { + KIS_SAFE_ASSERT_RECOVER(*dullingFillColor.colorSpace() == *m_preciseColorSpace) { + dullingFillColor.convertTo(m_preciseColorSpace); + } + + color.convertTo(m_preciseColorSpace); + + m_preciseColorRateCompositeOp->composite(dullingFillColor.data(), 0, + color.data(), 0, + 0, 0, + 1, 1, + m_colorRatePainter->opacity()); + } + } + + if (useDullingMode) { + KoColor realFillColor; + + if (m_useRoundingCorrection) { + const KoColor correctedFillColor = dullingFillColor.added(m_roundedColorError); + realFillColor = correctedFillColor.convertedTo(m_tempDev->colorSpace()); + + const KoColor roundedFillColor = realFillColor.convertedTo(m_preciseColorSpace); + m_roundedColorError = correctedFillColor.subtracted(roundedFillColor); + } else { + realFillColor = dullingFillColor.convertedTo(m_tempDev->colorSpace()); + } + + m_tempDev->fill(QRect(0, 0, m_dstDabRect.width(), m_dstDabRect.height()), realFillColor); } // if color is disabled (only smudge) and "overlay mode" is enabled