Commit 19e1baa9 authored by Dmitry Kazakov's avatar Dmitry Kazakov
Browse files

Merge branch 'kazakov/new-brush-from-voronwe'

parents ad1d00ed fb093f6d
......@@ -13,19 +13,67 @@ KisColorfulBrush::KisColorfulBrush(const QString &filename)
}
#include <KoColorSpaceMaths.h>
#include <KoColorSpaceTraits.h>
namespace {
qreal estimateImageAverage(const QImage &image) {
qint64 lightnessSum = 0;
qint64 alphaSum = 0;
for (int y = 0; y < image.height(); ++y) {
const QRgb *pixel = reinterpret_cast<const QRgb*>(image.scanLine(y));
for (int i = 0; i < image.width(); ++i) {
lightnessSum += qRound(qGray(*pixel) * qAlpha(*pixel) / 255.0);
alphaSum += qAlpha(*pixel);
pixel++;
}
}
return 255.0 * qreal(lightnessSum) / alphaSum;
}
}
qreal KisColorfulBrush::estimatedSourceMidPoint() const
{
return estimateImageAverage(KisBrush::brushTipImage());
}
qreal KisColorfulBrush::adjustedMidPoint() const
{
return estimateImageAverage(this->brushTipImage());
}
bool KisColorfulBrush::autoAdjustMidPoint() const
{
return m_autoAdjustMidPoint;
}
void KisColorfulBrush::setAutoAdjustMidPoint(bool autoAdjustMidPoint)
{
m_autoAdjustMidPoint = autoAdjustMidPoint;
}
QImage KisColorfulBrush::brushTipImage() const
{
QImage image = KisBrush::brushTipImage();
if (isImageType() && brushApplication() != IMAGESTAMP) {
if (m_adjustmentMidPoint != 127 ||
const qreal adjustmentMidPoint =
m_autoAdjustMidPoint ?
estimateImageAverage(image) :
m_adjustmentMidPoint;
if (qAbs(adjustmentMidPoint - 127.0) > 0.1 ||
!qFuzzyIsNull(m_brightnessAdjustment) ||
!qFuzzyIsNull(m_contrastAdjustment)) {
const int half = KoColorSpaceMathsTraits<quint8>::halfValue;
const int unit = KoColorSpaceMathsTraits<quint8>::unitValue;
const qreal midX = m_adjustmentMidPoint;
const qreal midX = adjustmentMidPoint;
const qreal midY = m_brightnessAdjustment > 0 ?
KoColorSpaceMaths<qreal>::blend(unit, half, m_brightnessAdjustment) :
KoColorSpaceMaths<qreal>::blend(0, half, -m_brightnessAdjustment);
......@@ -37,8 +85,13 @@ QImage KisColorfulBrush::brushTipImage() const
qreal hiB = 255.0;
if (!qFuzzyCompare(m_contrastAdjustment, 1.0)) {
loA = midY / (1.0 - m_contrastAdjustment) / midX;
hiA = (unit - midY) / (1.0 - m_contrastAdjustment) / (unit - midX);
if (m_contrastAdjustment > 0.0) {
loA = midY / (1.0 - m_contrastAdjustment) / midX;
hiA = (unit - midY) / (1.0 - m_contrastAdjustment) / (unit - midX);
} else {
loA = midY * (1.0 + m_contrastAdjustment) / midX;
hiA = (unit - midY) * (1.0 + m_contrastAdjustment) / (unit - midX);
}
loB = midY - midX * loA;
hiB = midY - midX * hiA;
......@@ -129,6 +182,7 @@ void KisColorfulBrush::toXML(QDomDocument& d, QDomElement& e) const
e.setAttribute("AdjustmentMidPoint", QString::number(m_adjustmentMidPoint));
e.setAttribute("BrightnessAdjustment", QString::number(m_brightnessAdjustment));
e.setAttribute("ContrastAdjustment", QString::number(m_contrastAdjustment));
e.setAttribute("AutoAdjustMidPoint", QString::number(m_autoAdjustMidPoint));
e.setAttribute("AdjustmentVersion", QString::number(2));
KisBrush::toXML(d, e);
}
......
......@@ -34,7 +34,14 @@ public:
void setHasColorAndTransparency(bool value);
bool hasColorAndTransparency() const;
qreal estimatedSourceMidPoint() const;
qreal adjustedMidPoint() const;
bool autoAdjustMidPoint() const;
virtual void setAutoAdjustMidPoint(bool autoAdjustMidPoint);
private:
bool m_autoAdjustMidPoint = false;
quint8 m_adjustmentMidPoint = 127;
qreal m_brightnessAdjustment = 0.0;
qreal m_contrastAdjustment = 0.0;
......
......@@ -176,6 +176,12 @@ public:
}
}
void setAutoAdjustMidPoint(bool value) {
Q_FOREACH (KisGbrBrushSP brush, m_brushes) {
brush->setAutoAdjustMidPoint(value);
}
}
void makeMaskImage(bool preserveAlpha) {
Q_FOREACH (KisGbrBrushSP brush, m_brushes) {
brush->makeMaskImage(preserveAlpha);
......@@ -394,7 +400,8 @@ QVector<KisGbrBrushSP> KisImagePipeBrush::brushes() const
KisFixedPaintDeviceSP KisImagePipeBrush::paintDevice(
const KoColorSpace * colorSpace,
KisDabShape const& shape,
const KisPaintInformation& info, double subPixelX, double subPixelY) const
const KisPaintInformation& info,
double subPixelX, double subPixelY) const
{
return d->brushesPipe.paintDevice(colorSpace, shape, info, subPixelX, subPixelY);
}
......@@ -429,6 +436,12 @@ void KisImagePipeBrush::setContrastAdjustment(qreal value)
d->brushesPipe.setContrastAdjustment(value);
}
void KisImagePipeBrush::setAutoAdjustMidPoint(bool value)
{
KisGbrBrush::setAutoAdjustMidPoint(value);
d->brushesPipe.setAutoAdjustMidPoint(value);
}
const KisBoundary* KisImagePipeBrush::boundary() const
{
KisGbrBrushSP brush = d->brushesPipe.firstBrush();
......
......@@ -71,6 +71,7 @@ public:
void setAdjustmentMidPoint(quint8 value) override;
void setBrightnessAdjustment(qreal value) override;
void setContrastAdjustment(qreal value) override;
void setAutoAdjustMidPoint(bool value) override;
QString parasiteSelection(); // returns random, constant, etc
......
......@@ -38,6 +38,7 @@ KisBrushSP KisPredefinedBrushFactory::createBrush(const QDomElement& brushDefini
}
if (!brush) {
qWarning() << "Using fallback brush" << ppVar(brushFileName);
brush = resourceSourceAdapter.fallbackResource();
brushtipFound = false;
}
......@@ -67,7 +68,8 @@ KisBrushSP KisPredefinedBrushFactory::createBrush(const QDomElement& brushDefini
qreal contrastAdjustment = brushDefinition.attribute("ContrastAdjustment").toDouble();
const int adjustmentVersion = brushDefinition.attribute("AdjustmentVersion", "1").toInt();
const bool autoAdjustMidPoint = brushDefinition.attribute("AutoAdjustMidPoint", "0").toInt();
const bool hasAutoAdjustMidPoint = brushDefinition.hasAttribute("AutoAdjustMidPoint");
/**
* In Krita 4.x releases there was a bug that caused lightness
......@@ -80,16 +82,28 @@ KisBrushSP KisPredefinedBrushFactory::createBrush(const QDomElement& brushDefini
* after applying a piecewice-linear function twice we get a
* quadratic function. So we fall-back to a blunt parameters scaling,
* which gives result that is just "good enough".
*
* NOTE: AutoAdjustMidPoint option appeared only in Krita 5, so it
* automatically means the adjustments should be applied in the new way.
*/
if (adjustmentVersion < 2) {
if (adjustmentVersion < 2 && !hasAutoAdjustMidPoint) {
adjustmentMidPoint = qBound(0, 127 + (int(adjustmentMidPoint) - 127) * 2, 255);
brightnessAdjustment *= 2.0;
contrastAdjustment *= 2.0;
/**
* In Krita we also changed formula for contrast calculation in
* negative part, so we need to convert that as well.
*/
if (contrastAdjustment < 0) {
contrastAdjustment = 1.0 / (1.0 - contrastAdjustment) - 1.0;
}
}
colorfulBrush->setAdjustmentMidPoint(adjustmentMidPoint);
colorfulBrush->setBrightnessAdjustment(brightnessAdjustment);
colorfulBrush->setContrastAdjustment(contrastAdjustment);
colorfulBrush->setAutoAdjustMidPoint(autoAdjustMidPoint);
}
auto legacyBrushApplication = [] (KisColorfulBrush *colorfulBrush, bool forceColorToAlpha) {
......
......@@ -246,7 +246,8 @@ void KisTextBrush::generateMaskAndApplyMaskOrCreateDab(KisFixedPaintDeviceSP dst
KisFixedPaintDeviceSP KisTextBrush::paintDevice(const KoColorSpace * colorSpace,
KisDabShape const& shape,
const KisPaintInformation& info, double subPixelX, double subPixelY) const
const KisPaintInformation& info,
double subPixelX, double subPixelY) const
{
if (brushType() == MASK) {
return KisBrush::paintDevice(colorSpace, shape, info, subPixelX, subPixelY);
......
......@@ -34,7 +34,7 @@ public:
{
}
~KoToolBasePrivate()
virtual ~KoToolBasePrivate()
{
Q_FOREACH (QPointer<QWidget> optionWidget, optionWidgets) {
if (optionWidget) {
......
......@@ -16,7 +16,7 @@ public:
: tool(t)
{
}
~KoInteractionStrategyPrivate()
virtual ~KoInteractionStrategyPrivate()
{
tool->setStatusText(QString());
}
......
......@@ -41,6 +41,7 @@ set(kritaglobal_LIB_SRCS
KisBezierUtils.cpp
KisBezierPatch.cpp
KisBezierMesh.cpp
KisRectsGrid.cpp
)
add_library(kritaglobal SHARED ${kritaglobal_LIB_SRCS} )
......
/*
* SPDX-FileCopyrightText: 2021 Dmitry Kazakov <dimula73@gmail.com>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "KisRectsGrid.h"
#include "kis_assert.h"
#include <QtCore/qmath.h>
#include "kis_lod_transform_base.h"
#include "kis_global.h"
#include "kis_algebra_2d.h"
#include "kis_debug.h"
KisRectsGrid::KisRectsGrid(int gridSize)
: m_gridSize(gridSize),
m_logGridSize(qFloor(std::log2(gridSize)))
{
KIS_ASSERT_RECOVER(qFuzzyCompare(std::log2(gridSize), qreal(m_logGridSize))) {
m_gridSize = 64;
m_logGridSize = 6;
}
}
void KisRectsGrid::resize(const QRect &newMappedAreaSize)
{
KIS_SAFE_ASSERT_RECOVER_NOOP(m_mappedAreaSize.isEmpty() || newMappedAreaSize.contains(m_mappedAreaSize));
QVector<quint8> newMapping(newMappedAreaSize.width() * newMappedAreaSize.height());
const int xDiff = m_mappedAreaSize.x() - newMappedAreaSize.x();
const int yDiff = m_mappedAreaSize.y() - newMappedAreaSize.y();
int dstRowStride = newMappedAreaSize.width();
int dstRowStart = xDiff + yDiff * dstRowStride;
for (int y = 0; y < m_mappedAreaSize.height(); y++) {
int dstRowIndex = dstRowStart + dstRowStride * y;
int srcRowIndex = m_mappedAreaSize.width() * y;
memcpy(&newMapping[dstRowIndex], &m_mapping[srcRowIndex], m_mappedAreaSize.width());
}
std::swap(newMapping, m_mapping);
m_mappedAreaSize = newMappedAreaSize;
}
QRect KisRectsGrid::alignRect(const QRect &rc) const
{
return KisLodTransformBase::alignedRect(rc, m_logGridSize);
}
QVector<QRect> KisRectsGrid::addRect(const QRect &rc)
{
return addAlignedRect(alignRect(rc));
}
QVector<QRect> KisRectsGrid::addAlignedRect(const QRect &rc)
{
if (rc.isEmpty()) return QVector<QRect>();
const QRect mappedRect = KisLodTransformBase::scaledRect(rc, m_logGridSize);
if (!m_mappedAreaSize.contains(mappedRect)) {
QRect nextMappingSize = m_mappedAreaSize | mappedRect;
nextMappingSize = KisAlgebra2D::blowRect(nextMappingSize, 0.2);
resize(nextMappingSize);
}
QVector<QRect> addedRects;
for (int y = mappedRect.y(); y <= mappedRect.bottom(); y++) {
for (int x = mappedRect.x(); x <= mappedRect.right(); x++) {
quint8 *ptr = &m_mapping[m_mappedAreaSize.width() * (y - m_mappedAreaSize.y()) + (x - m_mappedAreaSize.x())];
if (!*ptr) {
*ptr = 1;
addedRects.append(KisLodTransformBase::upscaledRect(QRect(x, y, 1, 1), m_logGridSize));
}
}
}
return addedRects;
}
inline QRect KisRectsGrid::shrinkRectToAlignedGrid(const QRect &srcRect, int lod)
{
qint32 alignment = 1 << lod;
qint32 x1, y1, x2, y2;
srcRect.getCoords(&x1, &y1, &x2, &y2);
x1--;
y1--;
x2++;
y2++;
KisLodTransformBase::alignByPow2ButOneHi(x1, alignment);
KisLodTransformBase::alignByPow2ButOneHi(y1, alignment);
KisLodTransformBase::alignByPow2Lo(x2, alignment);
KisLodTransformBase::alignByPow2Lo(y2, alignment);
x1++;
y1++;
x2--;
y2--;
QRect rect;
rect.setCoords(x1, y1, x2, y2);
return rect;
}
QVector<QRect> KisRectsGrid::removeRect(const QRect &rc)
{
const QRect alignedRect = shrinkRectToAlignedGrid(rc, m_logGridSize);
return !alignedRect.isEmpty() ? removeAlignedRect(alignedRect) : QVector<QRect>();
}
QVector<QRect> KisRectsGrid::removeAlignedRect(const QRect &rc)
{
const QRect mappedRect = KisLodTransformBase::scaledRect(rc, m_logGridSize);
// NOTE: we never shrink the size of the grid, just keep it as big as
// it ever was
QVector<QRect> removedRects;
for (int y = mappedRect.y(); y <= mappedRect.bottom(); y++) {
for (int x = mappedRect.x(); x <= mappedRect.right(); x++) {
quint8 *ptr = &m_mapping[m_mappedAreaSize.width() * (y - m_mappedAreaSize.y()) + (x - m_mappedAreaSize.x())];
if (*ptr) {
*ptr = 0;
removedRects.append(KisLodTransformBase::upscaledRect(QRect(x, y, 1, 1), m_logGridSize));
}
}
}
return removedRects;
}
bool KisRectsGrid::contains(const QRect &rc) const
{
const QRect mappedRect = KisLodTransformBase::scaledRect(alignRect(rc), m_logGridSize);
if (!m_mappedAreaSize.contains(mappedRect)) return false;
for (int y = mappedRect.y(); y <= mappedRect.bottom(); y++) {
for (int x = mappedRect.x(); x <= mappedRect.right(); x++) {
const quint8 *ptr = &m_mapping[m_mappedAreaSize.width() * (y - m_mappedAreaSize.y()) + (x - m_mappedAreaSize.x())];
if (!*ptr) return false;
}
}
return true;
}
QRect KisRectsGrid::boundingRect() const {
QRect gridBounds;
for (int y = m_mappedAreaSize.y(); y <= m_mappedAreaSize.bottom(); y++) {
for (int x = m_mappedAreaSize.x(); x <= m_mappedAreaSize.right(); x++) {
gridBounds |= QRect(x, y, 1, 1);
}
}
return KisLodTransformBase::upscaledRect(gridBounds, m_logGridSize);
}
/*
* SPDX-FileCopyrightText: 2021 Dmitry Kazakov <dimula73@gmail.com>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#ifndef KISRECTSGRID_H
#define KISRECTSGRID_H
#include <QRect>
#include <QVector>
#include "kritaglobal_export.h"
/**
* @brief A utility class to maintain a sparse grid of loaded/unloaded rects
*
* KisRectsGrid manages the presence of the rectangular cells in the grid
* covering some specific area. The main usecase of the class is to maintain
* an overlay device over another paint device.
*
* When you need to ensure that the overlay has some particular `rect` loaded,
* you just call `grid->addRect(rect)` and get a list of rects that have not
* yet been loaded into the overlay. The returned list may be empty if all the
* grid cells intersecting `rect` has already been loaded (added to the grid).
*
* The size of the cell is defined at the construction stage and must be
* power of 2.
*/
class KRITAGLOBAL_EXPORT KisRectsGrid
{
public:
/**
* Create a grid with cell size set to \p gridSize
*/
KisRectsGrid(int gridSize = 64);
/**
* Grow rectangle \p rc until it becomes aligned to
* the grid cell borders.
*/
QRect alignRect(const QRect &rc) const;
/**
* Add an arbitrary (non-aligned) rect to the grid
*
* The grid will form a list of cells that intersect \p rc and have not
* not yet been loaded, mark them as loaded and return the list to the caller.
*
* \param rc the rect to be added, not necessary aligned to the grid
* \return the list of cells that has actually been changed
*/
QVector<QRect> addRect(const QRect &rc);
/**
* Remove an arbitrary (non-aligned) rect from the grid
*
* The grid will form a list of loaded cells that are fully contained in \p
* rc, mark them as unloaded and return the list to the caller.
*
* TODO: please note that removing two neighbouring non-aligned rectangles
* may still leave some cells marked as loaded. Perhaps we should change
* the meaning of this function to remove "all intersecting rectangles"
* instead of "all contained rectangles".
*
* \param rc the rect to be removed, not necessary aligned to the grid
* \return the list of cells that has actually been changed
*/
QVector<QRect> removeRect(const QRect &rc);
/**
* Add an aligned rect to the grid
*
* The grid will form a list of cells that intersect \p rc and have not
* not yet been loaded, mark them as loaded and return the list to the caller.
*
* \param rc the rect to be added, the rect must be aligned
* \return the list of cells that has actually been changed
*/
QVector<QRect> addAlignedRect(const QRect &rc);
/**
* Remove an aligned rect from the grid
*
* The grid will form a list of loaded cells that are fully contained in \p
* rc, mark them as unloaded and return the list to the caller.
*
* \param rc the rect to be removed, not necessary aligned to the grid
* \return the list of cells that has actually been changed
*/
QVector<QRect> removeAlignedRect(const QRect &rc);
/**
* Return is \p rc is fully covered by the loaded cells of the grid
*/
bool contains(const QRect &rc) const;
/**
* Return the bounding box of the loaded cells of the grid
*/
QRect boundingRect() const;
private:
void resize(const QRect &newMappedAreaSize);
static QRect shrinkRectToAlignedGrid(const QRect &srcRect, int lod);
private:
int m_gridSize;
int m_logGridSize;
QVector<quint8> m_mapping;
QRect m_mappedAreaSize; // measured in col/row
};
#endif // KISRECTSGRID_H
......@@ -275,6 +275,13 @@ void KisRegion::approximateOverlappingRects(QVector<QRect> &rects, int gridSize)
}
}
void KisRegion::makeGridLikeRectsUnique(QVector<QRect> &rects)
{
std::sort(rects.begin(), rects.end(), detail::HorizontalMergePolicy::elementIsLess);
auto it = std::unique(rects.begin(), rects.end());
rects.erase(it, rects.end());
}
KisRegion::KisRegion(const QRect &rect)
{
m_rects << rect;
......
......@@ -51,6 +51,7 @@ public:
*/
static void approximateOverlappingRects(QVector<QRect> &rects, int gridSize);
static void makeGridLikeRectsUnique(QVector<QRect> &rects);
public:
KisRegion() = default;
......
......@@ -785,6 +785,57 @@ QPointF KRITAGLOBAL_EXPORT moveElasticPoint(const QPointF &pt,
const QPointF &base, const QPointF &newBase,
const QVector<QPointF> &anchorPoints);
/**
* @brief a simple class to generate Halton sequence
*
* This sequence of numbers can be used to sample areas
* in somewhat uniform way. See Wikipedia for more info:
*
* https://en.wikipedia.org/wiki/Halton_sequence
*/
class HaltonSequenceGenerator
{
public:
HaltonSequenceGenerator(int base)
: m_base(base)
{
}
int generate(int maxRange) {
generationStep();
return (m_n * maxRange + m_d / 2) / m_d;
}
qreal generate() {
generationStep();
return qreal(m_n) / m_d;
}
private:
inline void generationStep() {
int x = m_d - m_n;
if (x == 1) {
m_n = 1;
m_d *= m_base;
} else {
int y = m_d / m_base;
while (x <= y) {
y /= m_base;
}
m_n = (m_base + 1) * y - x;
}
}
private:
int m_n = 0;
int m_d = 1;
const int m_base = 0;
};