[calligra/krita-chili-kazakov] krita: Fixed dynamical updates of the Transform Mask

Dmitry Kazakov dimula73 at gmail.com
Thu Nov 13 10:37:10 UTC 2014


Git commit 09488d449e118d99b9a52d32ae80e0947e87a6ab by Dmitry Kazakov.
Committed on 13/11/2014 at 10:32.
Pushed by dkazakov into branch 'krita-chili-kazakov'.

Fixed dynamical updates of the Transform Mask

This patch implements numerous fixes and refactoings:

1) Implemented KisSafeTransform. It works like a usual QTransform but
   it takes maths into account. That is the transform and its reverse
   are *not* defined on the entire R^2 plane. Instead the valid input
   area is limited by the horizon line and nothing can be transformed
   above it. KisSafeTransform takes that into account and clips the
   desired rect/polygon to fit the valid area.

2) KisTransformMask::need/changeRect now uses safe trasnforms.

3) KisAsyncMerger recalculates the area of the clone's source to fetch
   correct data. To fix concurrency, this extra area is taken into account
   in KisCloneLayer::accessRect();

4) Implemented detailed unittests for dynamicat transform masks.

5) Added ability to store reference images of the unittest in a
   separate folder (outside repository).

   It consists of 3 major parts:

   1) checkQImageExternal() is expected to be used to fetch data from
      external folders only.

   2) KRITA_UNITTESTS_DATA_DIR environment variable is used to search for
      additional data sources

   3) KRITA_WRITE_UNITTESTS=1 together with KRITA_UNITTESTS_DATA_DIR set
      to a path will write the output of the unittest as a reference to
      an external folder.

6) The testing images are stored in:
	svn+ssh://svn@svn.kde.org/home/kde/trunk/tests/kritatests


CCMAIL:kimageshop at kde.org

M  +1    -0    krita/image/CMakeLists.txt
M  +8    -0    krita/image/kis_algebra_2d.cpp
M  +14   -1    krita/image/kis_algebra_2d.h
M  +12   -0    krita/image/kis_async_merger.cpp
M  +36   -2    krita/image/kis_clone_layer.cpp
M  +2    -0    krita/image/kis_clone_layer.h
M  +1    -1    krita/image/kis_perspectivetransform_worker.cpp
A  +200  -0    krita/image/kis_safe_transform.cpp     [License: GPL (v2+)]
A  +58   -0    krita/image/kis_safe_transform.h     [License: GPL (v2+)]
M  +38   -40   krita/image/kis_transform_mask.cpp
M  +5    -0    krita/image/kis_transform_mask_params_interface.cpp
M  +1    -0    krita/image/kis_transform_mask_params_interface.h
M  +378  -14   krita/image/tests/kis_transform_mask_test.cpp
M  +5    -1    krita/image/tests/kis_transform_mask_test.h
M  +121  -29   krita/sdk/tests/testutil.h

http://commits.kde.org/calligra/09488d449e118d99b9a52d32ae80e0947e87a6ab

diff --git a/krita/image/CMakeLists.txt b/krita/image/CMakeLists.txt
index 25419d1..d1304b7 100644
--- a/krita/image/CMakeLists.txt
+++ b/krita/image/CMakeLists.txt
@@ -136,6 +136,7 @@ set(kritaimage_LIB_SRCS
    kis_transform_mask_params_interface.cpp
    kis_recalculate_transform_mask_job.cpp
    kis_transform_mask_params_factory_registry.cpp
+   kis_safe_transform.cpp
    kis_gradient_painter.cc
    kis_iterator_ng.cpp
    kis_async_merger.cpp
diff --git a/krita/image/kis_algebra_2d.cpp b/krita/image/kis_algebra_2d.cpp
index 166ed89..3bb708a 100644
--- a/krita/image/kis_algebra_2d.cpp
+++ b/krita/image/kis_algebra_2d.cpp
@@ -124,4 +124,12 @@ QPainterPath smallArrow()
     return p;
 }
 
+QRect blowRect(const QRect &rect, qreal coeff)
+{
+    int w = rect.width() * coeff;
+    int h = rect.height() * coeff;
+
+    return rect.adjusted(-w, -h, w, h);
+}
+
 }
diff --git a/krita/image/kis_algebra_2d.h b/krita/image/kis_algebra_2d.h
index 8bd0e59..4f0c779 100644
--- a/krita/image/kis_algebra_2d.h
+++ b/krita/image/kis_algebra_2d.h
@@ -81,10 +81,17 @@ Point normalize(const Point &a)
  */
 template <typename T>
 T signPZ(T x) {
-    const T zeroValue(0);
     return x >= T(0) ? T(1) : T(-1);
 }
 
+/**
+ * Usual sign() function with zero returning zero
+ */
+template <typename T>
+T signZZ(T x) {
+    return x == T(0) ? T(0) : x > T(0) ? T(1) : T(-1);
+}
+
 template <class T>
 T leftUnitNormal(const T &a)
 {
@@ -209,6 +216,12 @@ inline Point clampPoint(Point pt, const Rect &bounds)
 
 QPainterPath KRITAIMAGE_EXPORT smallArrow();
 
+/**
+ * Multiply width and height of \p rect by \p coeff keeping the
+ * center of the rectangle pinned
+ */
+QRect KRITAIMAGE_EXPORT blowRect(const QRect &rect, qreal coeff);
+
 }
 
 #endif /* __KIS_ALGEBRA_2D_H */
diff --git a/krita/image/kis_async_merger.cpp b/krita/image/kis_async_merger.cpp
index c6737ac..74e13d6 100644
--- a/krita/image/kis_async_merger.cpp
+++ b/krita/image/kis_async_merger.cpp
@@ -150,6 +150,18 @@ public:
         QRegion prepareRegion(srcRect);
         prepareRegion -= m_cropRect;
 
+        QStack<QRect> applyRects;
+        bool rectVariesFlag;
+
+        /**
+         * If a clone has complicated masks, we should prepare additional
+         * source area to ensure the rect is prepared.
+         */
+        QRect needRectOnSource = layer->needRectOnSourceForMasks(srcRect);
+        if (!needRectOnSource.isEmpty()) {
+            prepareRegion += needRectOnSource;
+        }
+
         foreach(const QRect &rect, prepareRegion.rects()) {
             walker.collectRects(srcLayer, rect);
             merger.startMerge(walker, false);
diff --git a/krita/image/kis_clone_layer.cpp b/krita/image/kis_clone_layer.cpp
index 8f971ab..7018873 100644
--- a/krita/image/kis_clone_layer.cpp
+++ b/krita/image/kis_clone_layer.cpp
@@ -34,6 +34,9 @@
 #include "kis_clone_info.h"
 #include "kis_paint_layer.h"
 
+#include <QStack>
+#include <kis_effect_mask.h>
+
 
 struct KisCloneLayer::Private
 {
@@ -157,6 +160,28 @@ void KisCloneLayer::notifyParentVisibilityChanged(bool value)
     KisLayer::notifyParentVisibilityChanged(value);
 }
 
+QRect KisCloneLayer::needRectOnSourceForMasks(const QRect &rc) const
+{
+    QStack<QRect> applyRects_unused;
+    bool rectVariesFlag;
+
+    QList<KisEffectMaskSP> effectMasks = this->effectMasks();
+    if (effectMasks.isEmpty()) return QRect();
+
+    QRect needRect = this->masksNeedRect(effectMasks,
+                                         rc,
+                                         applyRects_unused,
+                                         rectVariesFlag);
+
+    if (needRect.isEmpty() ||
+        (!rectVariesFlag && needRect == rc)) {
+
+        return QRect();
+    }
+
+    return needRect;
+}
+
 qint32 KisCloneLayer::x() const
 {
     return m_d->x;
@@ -196,8 +221,17 @@ QRect KisCloneLayer::accessRect(const QRect &rect, PositionToFilthy pos) const
 {
     QRect resultRect = rect;
 
-    if(pos & (N_FILTHY_PROJECTION | N_FILTHY) && (m_d->x || m_d->y)) {
-        resultRect |= rect.translated(-m_d->x, -m_d->y);
+    if(pos & (N_FILTHY_PROJECTION | N_FILTHY)) {
+        if (m_d->x || m_d->y) {
+            resultRect |= rect.translated(-m_d->x, -m_d->y);
+        }
+
+        /**
+         * KisUpdateOriginalVisitor will try to recalculate some area
+         * on the clone's source, so this extra rectangle should also
+         * be taken into account
+         */
+        resultRect |= needRectOnSourceForMasks(rect);
     }
 
     return resultRect;
diff --git a/krita/image/kis_clone_layer.h b/krita/image/kis_clone_layer.h
index 1de10ff..7f22df2 100644
--- a/krita/image/kis_clone_layer.h
+++ b/krita/image/kis_clone_layer.h
@@ -116,6 +116,8 @@ public:
      */
     void setDirtyOriginal(const QRect &rect);
 
+    QRect needRectOnSourceForMasks(const QRect &rc) const;
+
 protected:
     void notifyParentVisibilityChanged(bool value);
 
diff --git a/krita/image/kis_perspectivetransform_worker.cpp b/krita/image/kis_perspectivetransform_worker.cpp
index 6db2ecc..8d45eb9 100644
--- a/krita/image/kis_perspectivetransform_worker.cpp
+++ b/krita/image/kis_perspectivetransform_worker.cpp
@@ -156,7 +156,7 @@ void KisPerspectiveTransformWorker::runPartialDst(KisPaintDeviceSP srcDev,
         return;
     }
 
-    QRectF srcClipRect = srcDev->defaultBounds()->bounds();
+    QRectF srcClipRect = srcDev->exactBounds();
 
     KisProgressUpdateHelper progressHelper(m_progressUpdater, 100, dstRect.height());
 
diff --git a/krita/image/kis_safe_transform.cpp b/krita/image/kis_safe_transform.cpp
new file mode 100644
index 0000000..490bdb6
--- /dev/null
+++ b/krita/image/kis_safe_transform.cpp
@@ -0,0 +1,200 @@
+/*
+ *  Copyright (c) 2014 Dmitry Kazakov <dimula73 at gmail.com>
+ *
+ *  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 "kis_safe_transform.h"
+
+#include <QTransform>
+#include <QLineF>
+#include <QPolygonF>
+
+
+#include "kis_debug.h"
+#include "kis_algebra_2d.h"
+
+
+
+struct KisSafeTransform::Private
+{
+    QRect bounds;
+    QTransform forwardTransform;
+    QTransform backwardTransform;
+
+    QPolygonF srcClipPolygon;
+    QPolygonF dstClipPolygon;
+
+    bool getHorizon(const QTransform &t, QLineF *horizon) {
+        static const qreal eps = 1e-10;
+
+        QPointF vanishingX(t.m11() / t.m13(), t.m12() / t.m13());
+        QPointF vanishingY(t.m21() / t.m23(), t.m22() / t.m23());
+
+        if (qAbs(t.m13()) < eps && qAbs(t.m23()) < eps) {
+            *horizon = QLineF();
+            return false;
+        } else if (qAbs(t.m23()) < eps) {
+            QPointF diff = t.map(QPointF(0.0, 10.0)) - t.map(QPointF());
+            vanishingY = vanishingX + diff;
+        } else if (qAbs(t.m13()) < eps) {
+            QPointF diff = t.map(QPointF(10.0, 0.0)) - t.map(QPointF());
+            vanishingX = vanishingY + diff;
+        }
+
+        *horizon = QLineF(vanishingX, vanishingY);
+        return true;
+    }
+
+    qreal getCrossSign(const QLineF &horizon, const QRectF &rc) {
+        if (rc.isEmpty()) return 1.0;
+
+        QPointF diff = horizon.p2() - horizon.p1();
+        return KisAlgebra2D::signPZ(KisAlgebra2D::crossProduct(diff, rc.center() - horizon.p1()));
+    }
+
+    QPolygonF getCroppedPolygon(const QLineF &baseHorizon, const QRect &rc, const qreal crossCoeff) {
+        if (rc.isEmpty()) return QPolygonF();
+
+        QRectF boundsRect(rc);
+        QPolygonF polygon(boundsRect);
+        QPolygonF result;
+
+        // calculate new (offset) horizon to avoid infinity
+        const qreal offsetLength = 10.0;
+        const QPointF horizonOffset = offsetLength * crossCoeff *
+            KisAlgebra2D::rightUnitNormal(baseHorizon.p2() - baseHorizon.p1());
+
+        const QLineF horizon = baseHorizon.translated(horizonOffset);
+
+        // base vectors to calculate the side of the horizon
+        const QPointF &basePoint = horizon.p1();
+        const QPointF horizonVec = horizon.p2() - basePoint;
+
+
+        // iteration
+        QPointF prevPoint = polygon[polygon.size() - 1];
+        qreal prevCross = crossCoeff * KisAlgebra2D::crossProduct(horizonVec, prevPoint - basePoint);
+
+        for (int i = 0; i < polygon.size(); i++) {
+            const QPointF &pt = polygon[i];
+
+            qreal cross = crossCoeff * KisAlgebra2D::crossProduct(horizonVec, pt - basePoint);
+
+            if ((cross >= 0 && prevCross >= 0) || (cross == 0 && prevCross < 0)) {
+                result << pt;
+            } else if (cross * prevCross < 0) {
+                QPointF intersection;
+                QLineF edge(prevPoint, pt);
+                QLineF::IntersectType intersectionType =
+                    horizon.intersect(edge, &intersection);
+
+                KIS_ASSERT_RECOVER_NOOP(intersectionType != QLineF::NoIntersection);
+
+                result << intersection;
+
+                if (cross > 0) {
+                    result << pt;
+                }
+            }
+
+            prevPoint = pt;
+            prevCross = cross;
+        }
+
+        if (!result.isClosed()) {
+            result << result.first();
+        }
+
+        return result;
+    }
+
+};
+
+KisSafeTransform::KisSafeTransform(const QTransform &transform,
+                                   const QRect &bounds,
+                                   const QRect &srcInterestRect)
+    : m_d(new Private)
+{
+    m_d->bounds = bounds;
+
+    m_d->forwardTransform = transform;
+    m_d->backwardTransform = transform.inverted();
+
+    m_d->srcClipPolygon = QPolygonF(QRectF(m_d->bounds));
+    m_d->dstClipPolygon = QPolygonF(QRectF(m_d->bounds));
+
+    qreal crossCoeff = 1.0;
+
+    QLineF srcHorizon;
+    if (m_d->getHorizon(m_d->backwardTransform, &srcHorizon)) {
+        crossCoeff = m_d->getCrossSign(srcHorizon, srcInterestRect);
+        m_d->srcClipPolygon = m_d->getCroppedPolygon(srcHorizon, m_d->bounds, crossCoeff);
+    }
+
+    QLineF dstHorizon;
+    if (m_d->getHorizon(m_d->forwardTransform, &dstHorizon)) {
+        crossCoeff = m_d->getCrossSign(dstHorizon, mapRectForward(srcInterestRect));
+        m_d->dstClipPolygon = m_d->getCroppedPolygon(dstHorizon, m_d->bounds, crossCoeff);
+    }
+
+}
+
+KisSafeTransform::~KisSafeTransform()
+{
+}
+
+QPolygonF KisSafeTransform::srcClipPolygon() const
+{
+    return m_d->srcClipPolygon;
+}
+
+QPolygonF KisSafeTransform::dstClipPolygon() const
+{
+    return m_d->dstClipPolygon;
+}
+
+QPolygonF KisSafeTransform::mapForward(const QPolygonF &p)
+{
+    QPolygonF poly = m_d->srcClipPolygon.intersected(p);
+    return m_d->forwardTransform.map(poly).intersected(QRectF(m_d->bounds));
+}
+
+QPolygonF KisSafeTransform::mapBackward(const QPolygonF &p)
+{
+    QPolygonF poly = m_d->dstClipPolygon.intersected(p);
+    return m_d->backwardTransform.map(poly).intersected(QRectF(m_d->bounds));
+}
+
+QRectF KisSafeTransform::mapRectForward(const QRectF &rc)
+{
+    return mapForward(rc).boundingRect();
+}
+
+QRectF KisSafeTransform::mapRectBackward(const QRectF &rc)
+{
+    return mapBackward(rc).boundingRect();
+}
+
+QRect KisSafeTransform::mapRectForward(const QRect &rc)
+{
+    return mapRectForward(QRectF(rc)).toAlignedRect();
+}
+
+QRect KisSafeTransform::mapRectBackward(const QRect &rc)
+{
+    return mapRectBackward(QRectF(rc)).toAlignedRect();
+}
+
diff --git a/krita/image/kis_safe_transform.h b/krita/image/kis_safe_transform.h
new file mode 100644
index 0000000..b958c67
--- /dev/null
+++ b/krita/image/kis_safe_transform.h
@@ -0,0 +1,58 @@
+/*
+ *  Copyright (c) 2014 Dmitry Kazakov <dimula73 at gmail.com>
+ *
+ *  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_SAFE_TRANSFORM_H
+#define __KIS_SAFE_TRANSFORM_H
+
+#include <QScopedPointer>
+
+#include "krita_export.h"
+
+class QTransform;
+class QRect;
+class QRectF;
+class QPolygonF;
+
+
+class KRITAIMAGE_EXPORT KisSafeTransform
+{
+public:
+    KisSafeTransform(const QTransform &transform,
+                     const QRect &bounds,
+                     const QRect &srcInterestRect);
+
+    ~KisSafeTransform();
+
+    QPolygonF srcClipPolygon() const;
+    QPolygonF dstClipPolygon() const;
+
+    QPolygonF mapForward(const QPolygonF &p);
+    QPolygonF mapBackward(const QPolygonF &p);
+
+    QRectF mapRectForward(const QRectF &rc);
+    QRectF mapRectBackward(const QRectF &rc);
+
+    QRect mapRectForward(const QRect &rc);
+    QRect mapRectBackward(const QRect &rc);
+
+private:
+    struct Private;
+    const QScopedPointer<Private> m_d;
+};
+
+#endif /* __KIS_SAFE_TRANSFORM_H */
diff --git a/krita/image/kis_transform_mask.cpp b/krita/image/kis_transform_mask.cpp
index 0b40569..35a4cd7 100644
--- a/krita/image/kis_transform_mask.cpp
+++ b/krita/image/kis_transform_mask.cpp
@@ -39,6 +39,10 @@
 #include "kis_transform_mask_params_interface.h"
 #include "kis_recalculate_transform_mask_job.h"
 #include "kis_signal_compressor.h"
+#include "kis_algebra_2d.h"
+#include "kis_safe_transform.h"
+
+
 
 #define UPDATE_DELAY 3000 /*ms */
 
@@ -165,7 +169,7 @@ void KisTransformMask::recaclulateStaticImage()
      * into account all the change rects of all the masks. Usually,
      * this work is done by the walkers.
      */
-    QRect requestedRect = parentLayer->changeRect(parentLayer->exactBounds());
+    QRect requestedRect = parentLayer->changeRect(parentLayer->original()->exactBounds());
     parentLayer->updateProjection(requestedRect, N_FILTHY_PROJECTION);
     m_d->recalculatingStaticImage = false;
 
@@ -215,14 +219,6 @@ void KisTransformMask::accept(KisProcessingVisitor &visitor, KisUndoAdapter *und
     return visitor.visit(this, undoAdapter);
 }
 
-QRect calculateLimitingRect(const QRect &bounds, qreal coeff)
-{
-    int w = bounds.width() * coeff;
-    int h = bounds.height() * coeff;
-
-    return bounds.adjusted(-w, -h, w, h);
-}
-
 QRect KisTransformMask::changeRect(const QRect &rect, PositionToFilthy pos) const
 {
     Q_UNUSED(pos);
@@ -234,33 +230,26 @@ QRect KisTransformMask::changeRect(const QRect &rect, PositionToFilthy pos) cons
     if (rect.isEmpty()) return rect;
     if (!m_d->params->isAffine()) return rect;
 
-    QRect changeRect = m_d->worker.forwardTransform()
-        .mapRect(QRectF(rect)).toAlignedRect();
-
-    KisNodeSP parentNode;
-    KisPaintDeviceSP parentOriginal;
-
-    if ((parentNode = parent()) &&
-        (parentOriginal = parentNode->original())) {
-
-        const QRect bounds = parentOriginal->defaultBounds()->bounds();
-        const QRect limitingRect = calculateLimitingRect(bounds, 2);
+    QRect bounds;
+    QRect interestRect;
+    KisNodeSP parentNode = parent();
 
-        changeRect &= limitingRect;
-        QRect backwardRect = limitingRect & m_d->worker.backwardTransform().mapRect(rect);
-
-        QRegion backwardRegion(backwardRect);
-        backwardRegion -= bounds;
-        backwardRegion = m_d->worker.forwardTransform().map(backwardRegion);
-
-        // FIXME: d-oh... please fix me and use region instead :(
-        changeRect |= backwardRegion.boundingRect();
+    if (parentNode) {
+        bounds = parentNode->original()->defaultBounds()->bounds();
+        interestRect = parentNode->original()->extent();
     } else {
-        qWarning() << "WARNING: a transform mask has no parent, don't know how to limit it";
-        const QRect limitingRect(-1000, -1000, 10000, 10000);
-        changeRect &= limitingRect;
+        bounds = QRect(0,0,777,777);
+        interestRect = QRect(0,0,888,888);
+        qWarning() << "WARNING: transform mask has no parent (change rect)."
+                   << "Cannot run safe transformations."
+                   << "Will limit bounds to" << ppVar(bounds);
     }
 
+    const QRect limitingRect = KisAlgebra2D::blowRect(bounds, 0.5);
+
+    KisSafeTransform transform(m_d->worker.forwardTransform(), limitingRect, interestRect);
+    QRect changeRect = transform.mapRectForward(rect);
+
     return changeRect;
 }
 
@@ -275,17 +264,26 @@ QRect KisTransformMask::needRect(const QRect& rect, PositionToFilthy pos) const
     if (rect.isEmpty()) return rect;
     if (!m_d->params->isAffine()) return rect;
 
-    QRect needRect = kisGrowRect(m_d->worker.backwardTransform().mapRect(rect), 2);
+    QRect bounds;
+    QRect interestRect;
+    KisNodeSP parentNode = parent();
 
-    KisNodeSP parentNode;
-
-    if ((parentNode = parent())) {
-        needRect &= parentNode->extent();
-    } else if (needRect.width() > 1e6 || needRect.height() > 1e6) {
-        qWarning() << "WARNING: transform mask returns infinite need rect! Dropping..." << needRect;
-        needRect = rect;
+    if (parentNode) {
+        bounds = parentNode->original()->defaultBounds()->bounds();
+        interestRect = parentNode->original()->extent();
+    } else {
+        bounds = QRect(0,0,777,777);
+        interestRect = QRect(0,0,888,888);
+        qWarning() << "WARNING: transform mask has no parent (need rect)."
+                   << "Cannot run safe transformations."
+                   << "Will limit bounds to" << ppVar(bounds);
     }
 
+    const QRect limitingRect = KisAlgebra2D::blowRect(bounds, 0.5);
+
+    KisSafeTransform transform(m_d->worker.forwardTransform(), limitingRect, interestRect);
+    QRect needRect = transform.mapRectBackward(rect);
+
     return needRect;
 }
 
diff --git a/krita/image/kis_transform_mask_params_interface.cpp b/krita/image/kis_transform_mask_params_interface.cpp
index a9c7e67..becc5cc 100644
--- a/krita/image/kis_transform_mask_params_interface.cpp
+++ b/krita/image/kis_transform_mask_params_interface.cpp
@@ -117,6 +117,11 @@ QTransform KisDumbTransformMaskParams::testingGetTransform() const
     return m_d->transform;
 }
 
+void KisDumbTransformMaskParams::testingSetTransform(const QTransform &t)
+{
+    m_d->transform = t;
+}
+
 #include "kis_transform_mask_params_factory_registry.h"
 
 struct DumbParamsRegistrar {
diff --git a/krita/image/kis_transform_mask_params_interface.h b/krita/image/kis_transform_mask_params_interface.h
index 7300c01..f9ac651 100644
--- a/krita/image/kis_transform_mask_params_interface.h
+++ b/krita/image/kis_transform_mask_params_interface.h
@@ -64,6 +64,7 @@ public:
 
     // for tesing purposes only
     QTransform testingGetTransform() const;
+    void testingSetTransform(const QTransform &t);
 
 private:
     struct Private;
diff --git a/krita/image/tests/kis_transform_mask_test.cpp b/krita/image/tests/kis_transform_mask_test.cpp
index b1719d2..1e195f0 100644
--- a/krita/image/tests/kis_transform_mask_test.cpp
+++ b/krita/image/tests/kis_transform_mask_test.cpp
@@ -19,41 +19,405 @@
 #include "kis_transform_mask_test.h"
 
 #include <qtest_kde.h>
+
+#include <KoColor.h>
+
+
 #include "kis_transform_mask.h"
 #include "kis_transform_mask_params_interface.h"
 
 #include "testutil.h"
 
+#include "kis_algebra_2d.h"
+#include "kis_safe_transform.h"
+#include "kis_clone_layer.h"
+
+
+
+inline QString toOctaveFormat(const QTransform &t)
+{
+    QString s("T = [%1 %2 %3; %4 %5 %6; %7 %8 %9]");
+    s = s
+        .arg(t.m11()).arg(t.m12()).arg(t.m13())
+        .arg(t.m21()).arg(t.m22()).arg(t.m23())
+        .arg(t.m31()).arg(t.m32()).arg(t.m33());
+
+    return s;
+}
+
+void KisTransformMaskTest::testSafeTransform()
+{
+    QTransform transform(-0.177454, -0.805953, -0.00213713,
+                         -1.9295, -0.371835, -0.00290463,
+                         3075.05, 2252.32, 7.62371);
+
+    QRectF testRect(0, 1024, 512, 512);
+    KisSafeTransform t2(transform, QRect(0, 0, 2048, 2048), testRect.toRect());
+
+    QPolygonF fwdPoly = t2.mapForward(testRect);
+    QRectF fwdRect = t2.mapRectForward(testRect);
+
+    QPolygonF bwdPoly = t2.mapBackward(fwdPoly);
+    QRectF bwdRect = t2.mapRectBackward(fwdRect);
+
+    QPolygon ref;
+
+    ref.clear();
+    ref << QPoint(284, 410);
+    ref << QPoint(10, 613);
+    ref << QPoint(35, 532);
+    ref << QPoint(236, 403);
+    ref << QPoint(284, 410);
+    QCOMPARE(fwdPoly.toPolygon(), ref);
+    QCOMPARE(fwdRect.toRect(), QRect(10,403,274,211));
+
+    ref.clear();
+    ref << QPoint(512, 1024);
+    ref << QPoint(512, 1536);
+    ref << QPoint(0, 1536);
+    ref << QPoint(0, 1024);
+    ref << QPoint(512, 1024);
+    QCOMPARE(bwdPoly.toPolygon(), ref);
+    QCOMPARE(bwdRect.toRect(), QRect(0, 994, 1198, 584));
+
+/*
+    QImage image(2500, 2500, QImage::Format_ARGB32);
+    QPainter gc(&image);
+    gc.setPen(Qt::cyan);
+
+    gc.setOpacity(0.7);
+
+    gc.setBrush(Qt::red);
+    gc.drawPolygon(t2.srcClipPolygon());
+
+    gc.setBrush(Qt::green);
+    gc.drawPolygon(t2.dstClipPolygon());
+
+    qDebug() << ppVar(testRect);
+    qDebug() << ppVar(fwdPoly);
+    qDebug() << ppVar(fwdRect);
+    qDebug() << ppVar(bwdPoly);
+    qDebug() << ppVar(bwdRect);
+
+    gc.setBrush(Qt::yellow);
+    gc.drawPolygon(testRect);
+
+    gc.setBrush(Qt::red);
+    gc.drawPolygon(fwdRect);
+    gc.setBrush(Qt::blue);
+    gc.drawPolygon(fwdPoly);
+
+    gc.setBrush(Qt::magenta);
+    gc.drawPolygon(bwdRect);
+    gc.setBrush(Qt::cyan);
+    gc.drawPolygon(bwdPoly);
+
+    gc.end();
+    image.save("polygons_safety.png");
+*/
+}
+
+void KisTransformMaskTest::testSafeTransformUnity()
+{
+    QTransform transform;
+
+    QRectF testRect(0, 1024, 512, 512);
+    KisSafeTransform t2(transform, QRect(0, 0, 2048, 2048), testRect.toRect());
+
+    QPolygonF fwdPoly = t2.mapForward(testRect);
+    QRectF fwdRect = t2.mapRectForward(testRect);
+
+    QPolygonF bwdPoly = t2.mapBackward(fwdPoly);
+    QRectF bwdRect = t2.mapRectBackward(fwdRect);
+
+    QCOMPARE(testRect, fwdRect);
+    QCOMPARE(testRect, bwdRect);
+    QCOMPARE(fwdPoly, QPolygonF(testRect));
+    QCOMPARE(bwdPoly, QPolygonF(testRect));
+}
+
+void KisTransformMaskTest::testSafeTransformSingleVanishingPoint()
+{
+    // rotation around 0X has a single vanishing point for 0Y axis
+    QTransform transform(1, 0, 0,
+                         -0.870208, -0.414416, -0.000955222,
+                         132.386, 1082.91, 1.99439);
+
+    QTransform R; R.rotateRadians(M_PI / 4.0);
+    //transform *= R;
+
+    QRectF testRect(1536, 1024, 512, 512);
+    KisSafeTransform t2(transform, QRect(0, 0, 2048, 2048), testRect.toRect());
+
+    QPolygonF fwdPoly = t2.mapForward(testRect);
+    QRectF fwdRect = t2.mapRectForward(testRect);
+
+    QPolygonF bwdPoly = t2.mapBackward(fwdPoly);
+    QRectF bwdRect = t2.mapRectBackward(fwdRect);
+
+    /**
+     * A special weird rect that crosses the vanishing point,
+     * which is (911.001, 433.84) in this case
+     */
+    QRectF fwdNastyRect(800, 100, 400, 600);
+    //QRectF fwdNastyRect(100, 400, 1000, 800);
+    QRectF bwdNastyRect = t2.mapRectBackward(fwdNastyRect);
+
+/*
+    qDebug() << ppVar(testRect);
+    qDebug() << ppVar(fwdPoly);
+    qDebug() << ppVar(fwdRect);
+    qDebug() << ppVar(bwdPoly);
+    qDebug() << ppVar(bwdRect);
+    qDebug() << ppVar(bwdNastyRect);
+*/
+
+    QPolygon ref;
+
+    ref.clear();
+    ref << QPoint(765,648);
+    ref << QPoint(1269, 648);
+    ref << QPoint(1601, 847);
+    ref << QPoint(629, 847);
+    ref << QPoint(765, 648);
+    QCOMPARE(fwdPoly.toPolygon(), ref);
+    QCOMPARE(fwdRect.toRect(), QRect(629,648,971,199));
+
+    ref.clear();
+    ref << QPoint(1536,1024);
+    ref << QPoint(2048,1024);
+    ref << QPoint(2048,1536);
+    ref << QPoint(1536,1536);
+    ref << QPoint(1536,1024);
+    QCOMPARE(bwdPoly.toPolygon(), ref);
+    QCOMPARE(bwdRect.toRect(), QRect(1398,1024,650,512));
+
+    QCOMPARE(bwdNastyRect.toRect(), QRect(1463,0,585,1232));
+}
+
+bool checkImage(KisImageSP image, const QString &testName, const QString &prefix) {
+    return TestUtil::checkQImageExternal(image->projection()->convertToQImage(0, image->bounds()),
+                                         "transform_mask_updates",
+                                         prefix,
+                                         testName, 1, 1, 100);
+}
+
+bool doPartialTests(const QString &prefix, KisImageSP image, KisLayerSP paintLayer,
+                    KisLayerSP visibilityToggleLayer, KisTransformMaskSP mask)
+{
+    bool result = true;
+
+    QRect refRect = image->bounds();
+
+    int testIndex = 1;
+    QString testName;
+
+    for (int y = 0; y < refRect.height(); y += 512) {
+        for (int x = 0; x < refRect.width(); x += 512) {
+            QRect rc(x, y, 512, 512);
+
+            if (rc.right() > refRect.right()) {
+                rc.setRight(refRect.right());
+                if (rc.isEmpty()) continue;
+            }
+
+            if (rc.bottom() > refRect.bottom()) {
+                rc.setBottom(refRect.bottom());
+                if (rc.isEmpty()) continue;
+            }
+
+            paintLayer->setDirty(rc);
+            image->waitForDone();
+            testName = QString("tm_%1_partial_%2_%3").arg(testIndex++).arg(x).arg(y);
+            result &= checkImage(image, testName, prefix);
+        }
+    }
+
+    // initial update of the mask to clear the unused portions of the projection
+    // (it updates only when we call set dirty on the mask itself, which happens
+    // in Krita right after the addition of the mask onto a layer)
+
+    mask->setDirty();
+    image->waitForDone();
+    testName = QString("tm_%1_initial_mask_visible_on").arg(testIndex++);
+    result &= checkImage(image, testName, prefix);
+
+    // start layer visibility testing
+
+    paintLayer->setVisible(false);
+    paintLayer->setDirty();
+    image->waitForDone();
+    testName = QString("tm_%1_layer_visible_off").arg(testIndex++);
+    result &= checkImage(image, testName, prefix);
+
+    paintLayer->setVisible(true);
+    paintLayer->setDirty();
+    image->waitForDone();
+    testName = QString("tm_%1_layer_visible_on").arg(testIndex++);
+    result &= checkImage(image, testName, prefix);
+
+    if (paintLayer != visibilityToggleLayer) {
+        visibilityToggleLayer->setVisible(false);
+        visibilityToggleLayer->setDirty();
+        image->waitForDone();
+        testName = QString("tm_%1_extra_layer_visible_off").arg(testIndex++);
+        result &= checkImage(image, testName, prefix);
+
+
+        visibilityToggleLayer->setVisible(true);
+        visibilityToggleLayer->setDirty();
+        image->waitForDone();
+        testName = QString("tm_%1_extra_layer_visible_on").arg(testIndex++);
+        result &= checkImage(image, testName, prefix);
+    }
+
+    // toggle mask visibility
+
+    mask->setVisible(false);
+    mask->setDirty();
+    image->waitForDone();
+    testName = QString("tm_%1_mask_visible_off").arg(testIndex++);
+    result &= checkImage(image, testName, prefix);
+
+    mask->setVisible(true);
+    mask->setDirty();
+    image->waitForDone();
+    testName = QString("tm_%1_mask_visible_on").arg(testIndex++);
+    result &= checkImage(image, testName, prefix);
+
+    // entire bounds update
+
+    // no clearing, just don't hang up
+
+    paintLayer->setDirty(refRect);
+    image->waitForDone();
+    testName = QString("tm_%1_layer_dirty_bounds").arg(testIndex++);
+    result &= checkImage(image, testName, prefix);
+
+    // no clearing, just don't hang up
+
+    mask->setDirty(refRect);
+    image->waitForDone();
+    testName = QString("tm_%1_mask_dirty_bounds").arg(testIndex++);
+    result &= checkImage(image, testName, prefix);
+
+    if (paintLayer != visibilityToggleLayer) {
+        // no clearing, just don't hang up
+
+        visibilityToggleLayer->setDirty(refRect);
+        image->waitForDone();
+        testName = QString("tm_%1_extra_layer_dirty_bounds").arg(testIndex++);
+        result &= checkImage(image, testName, prefix);
+    }
+
+    QRect fillRect;
+
+    // partial updates outside
+
+    fillRect = QRect(-100, 0.5 * refRect.height(), 50, 100);
+    paintLayer->paintDevice()->fill(fillRect, KoColor(Qt::red, image->colorSpace()));
+    paintLayer->setDirty(fillRect);
+    image->waitForDone();
+    testName = QString("tm_%1_layer_dirty_outside_%2_%3").arg(testIndex++).arg(fillRect.x()).arg(fillRect.y());
+    result &= checkImage(image, testName, prefix);
 
-void KisTransformMaskTest::test()
+    fillRect = QRect(0.5 * refRect.width(), -100, 100, 50);
+    paintLayer->paintDevice()->fill(fillRect, KoColor(Qt::red, image->colorSpace()));
+    paintLayer->setDirty(fillRect);
+    image->waitForDone();
+    testName = QString("tm_%1_layer_dirty_outside_%2_%3").arg(testIndex++).arg(fillRect.x()).arg(fillRect.y());
+    result &= checkImage(image, testName, prefix);
+
+    fillRect = QRect(refRect.width() + 50, 0.2 * refRect.height(), 50, 100);
+    paintLayer->paintDevice()->fill(fillRect, KoColor(Qt::red, image->colorSpace()));
+    paintLayer->setDirty(fillRect);
+    image->waitForDone();
+    testName = QString("tm_%1_layer_dirty_outside_%2_%3").arg(testIndex++).arg(fillRect.x()).arg(fillRect.y());
+    result &= checkImage(image, testName, prefix);
+
+    // partial update inside
+
+    fillRect = QRect(0.5 * refRect.width() - 50, 0.5 * refRect.height() - 50, 100, 100);
+    paintLayer->paintDevice()->fill(fillRect, KoColor(Qt::red, image->colorSpace()));
+    paintLayer->setDirty(fillRect);
+    image->waitForDone();
+    testName = QString("tm_%1_layer_dirty_inside_%2_%3").arg(testIndex++).arg(fillRect.x()).arg(fillRect.y());
+    result &= checkImage(image, testName, prefix);
+
+    // clear explicitly
+    image->projection()->clear();
+
+    mask->setDirty();
+    image->waitForDone();
+    testName = QString("tm_%1_mask_dirty_bounds").arg(testIndex++);
+    result &= checkImage(image, testName, prefix);
+
+
+    KisDumbTransformMaskParams *params =
+        dynamic_cast<KisDumbTransformMaskParams*>(mask->transformParams().data());
+
+    QTransform t = params->testingGetTransform();
+    t *= QTransform::fromTranslate(400, 300);
+    params->testingSetTransform(t);
+    mask->setTransformParams(mask->transformParams());
+
+    mask->setDirty();
+    image->waitForDone();
+    testName = QString("tm_%1_mask_dirty_after_offset").arg(testIndex++);
+    result &= checkImage(image, testName, prefix);
+
+    return result;
+}
+
+void KisTransformMaskTest::testMaskOnPaintLayer()
 {
     QImage refImage(TestUtil::fetchDataFileLazy("test_transform_quality.png"));
-    TestUtil::MaskParent p(refImage.rect());
+    QRect refRect = refImage.rect();
+    TestUtil::MaskParent p(refRect);
 
     p.layer->paintDevice()->convertFromQImage(refImage, 0);
 
-    KisTransformMaskSP mask = new KisTransformMask();
+    KisPaintLayerSP player = new KisPaintLayer(p.image, "bg", OPACITY_OPAQUE_U8, p.image->colorSpace());
+    p.image->addNode(player, p.image->root(), KisNodeSP());
 
+    KisTransformMaskSP mask = new KisTransformMask();
     p.image->addNode(mask, p.layer);
 
-    QTransform transform =
-        QTransform::fromTranslate(100.0, 0.0) *
-        QTransform::fromScale(2.0, 1.0);
+    QTransform transform(-0.177454, -0.805953, -0.00213713,
+                         -1.9295, -0.371835, -0.00290463,
+                         3075.05, 2252.32, 7.62371);
 
     mask->setTransformParams(KisTransformMaskParamsInterfaceSP(
                                  new KisDumbTransformMaskParams(transform)));
 
-    p.layer->setDirty(QRect(160, 160, 150, 300));
-    p.layer->setDirty(QRect(310, 160, 150, 300));
-    p.layer->setDirty(QRect(460, 160, 150, 300));
-    p.layer->setDirty(QRect(610, 160, 150, 300));
-    p.layer->setDirty(QRect(760, 160, 150, 300));
+    QVERIFY(doPartialTests("pl", p.image, p.layer, p.layer, mask));
+}
+
+void KisTransformMaskTest::testMaskOnCloneLayer()
+{
+    QImage refImage(TestUtil::fetchDataFileLazy("test_transform_quality.png"));
+    QRect refRect = refImage.rect();
+    TestUtil::MaskParent p(refRect);
+
+    p.layer->paintDevice()->convertFromQImage(refImage, 0);
 
-    p.image->waitForDone();
+    KisPaintLayerSP player = new KisPaintLayer(p.image, "bg", OPACITY_OPAQUE_U8, p.image->colorSpace());
+    p.image->addNode(player, p.image->root(), KisNodeSP());
 
-    QImage result = p.layer->projection()->convertToQImage(0);
-    TestUtil::checkQImage(result, "transform_mask_test", "partial", "single");
+    KisCloneLayerSP clone = new KisCloneLayer(p.layer, p.image, "clone", OPACITY_OPAQUE_U8);
+    p.image->addNode(clone, p.image->root());
+
+    KisTransformMaskSP mask = new KisTransformMask();
+    p.image->addNode(mask, clone);
+
+    QTransform transform(-0.177454, -0.805953, -0.00213713,
+                         -1.9295, -0.371835, -0.00290463,
+                         3075.05, 2252.32, 7.62371);
+
+    mask->setTransformParams(KisTransformMaskParamsInterfaceSP(
+                                 new KisDumbTransformMaskParams(transform)));
 
+    QVERIFY(doPartialTests("cl", p.image, p.layer, clone, mask));
 }
 
 QTEST_KDEMAIN(KisTransformMaskTest, GUI)
diff --git a/krita/image/tests/kis_transform_mask_test.h b/krita/image/tests/kis_transform_mask_test.h
index 32afb01..a534bb8 100644
--- a/krita/image/tests/kis_transform_mask_test.h
+++ b/krita/image/tests/kis_transform_mask_test.h
@@ -25,7 +25,11 @@ class KisTransformMaskTest : public QObject
 {
     Q_OBJECT
 private slots:
-    void test();
+    void testSafeTransform();
+    void testMaskOnPaintLayer();
+    void testMaskOnCloneLayer();
+    void testSafeTransformUnity();
+    void testSafeTransformSingleVanishingPoint();
 };
 
 #endif /* __KIS_TRANSFORM_MASK_TEST_H */
diff --git a/krita/sdk/tests/testutil.h b/krita/sdk/tests/testutil.h
index 651a18f..2e12b27 100644
--- a/krita/sdk/tests/testutil.h
+++ b/krita/sdk/tests/testutil.h
@@ -63,24 +63,51 @@ inline KisNodeSP findNode(KisNodeSP root, const QString &name) {
     return 0;
 }
 
-inline QString fetchDataFileLazy(const QString relativeFileName)
+#include <QProcessEnvironment>
+
+inline QString fetchExternalDataFileName(const QString relativeFileName)
 {
+    static QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
+    static QString unittestsDataDirPath = "KRITA_UNITTESTS_DATA_DIR";
+
+    QString path;
+    if (!env.contains(unittestsDataDirPath)) {
+        qWarning() << "Environment variable" << unittestsDataDirPath << "is not set";
+        return QString();
+    } else {
+        path = env.value(unittestsDataDirPath, "");
+    }
+
     QString filename  =
-        QString(FILES_DATA_DIR) +
+        path +
         QDir::separator() +
         relativeFileName;
 
-    if (QFileInfo(filename).exists()) {
-        return filename;
-    }
+    return filename;
+}
 
-    filename  =
-        QString(FILES_DEFAULT_DATA_DIR) +
-        QDir::separator() +
-        relativeFileName;
+inline QString fetchDataFileLazy(const QString relativeFileName, bool externalTest = false)
+{
+    if (externalTest) {
+        return fetchExternalDataFileName(relativeFileName);
+    } else {
+        QString filename  =
+            QString(FILES_DATA_DIR) +
+            QDir::separator() +
+            relativeFileName;
+
+        if (QFileInfo(filename).exists()) {
+            return filename;
+        }
+
+        filename  =
+            QString(FILES_DEFAULT_DATA_DIR) +
+            QDir::separator() +
+            relativeFileName;
 
-    if (QFileInfo(filename).exists()) {
-        return filename;
+        if (QFileInfo(filename).exists()) {
+            return filename;
+        }
     }
 
     return QString();
@@ -135,7 +162,7 @@ private:
 };
 
 
-inline bool compareQImages(QPoint & pt, const QImage & image1, const QImage & image2, int fuzzy = 0, int fuzzyAlpha = 0)
+inline bool compareQImages(QPoint & pt, const QImage & image1, const QImage & image2, int fuzzy = 0, int fuzzyAlpha = 0, int maxNumFailingPixels = 0)
 {
     //     QTime t;
     //     t.start();
@@ -153,6 +180,8 @@ inline bool compareQImages(QPoint & pt, const QImage & image1, const QImage & im
         return false;
     }
 
+    int numFailingPixels = 0;
+
     for (int y = 0; y < h1; ++y) {
         const QRgb * const firstLine = reinterpret_cast<const QRgb *>(image2.scanLine(y));
         const QRgb * const secondLine = reinterpret_cast<const QRgb *>(image1.scanLine(y));
@@ -170,11 +199,19 @@ inline bool compareQImages(QPoint & pt, const QImage & image1, const QImage & im
                 if (!bothTransparent && (!same || !sameAlpha)) {
                     pt.setX(x);
                     pt.setY(y);
+                    numFailingPixels++;
+
                     qDebug() << " Different at" << pt
                              << "source" << qRed(a) << qGreen(a) << qBlue(a) << qAlpha(a)
                              << "dest" << qRed(b) << qGreen(b) << qBlue(b) << qAlpha(b)
-                             << "fuzzy" << fuzzy;
-                    return false;
+                             << "fuzzy" << fuzzy
+                             << "fuzzyAlpha" << fuzzyAlpha
+                             << "(" << numFailingPixels << "of" << maxNumFailingPixels << "allowed )";
+
+
+                    if (numFailingPixels > maxNumFailingPixels) {
+                        return false;
+                    }
                 }
             }
         }
@@ -257,44 +294,99 @@ inline bool comparePaintDevicesClever(const KisPaintDeviceSP dev1, const KisPain
 
 #ifdef FILES_OUTPUT_DIR
 
-inline bool checkQImage(const QImage &image, const QString &testName,
-                        const QString &prefix, const QString &name,
-                        int fuzzy = 0)
+inline bool checkQImageImpl(bool externalTest,
+                            const QImage &image, const QString &testName,
+                            const QString &prefix, const QString &name,
+                            int fuzzy, int fuzzyAlpha, int maxNumFailingPixels)
 {
-    Q_UNUSED(fuzzy);
+    if (fuzzyAlpha == -1) {
+        fuzzyAlpha = fuzzy;
+    }
+
+
     QString filename(prefix + "_" + name + ".png");
     QString dumpName(prefix + "_" + name + "_expected.png");
 
-    QString fullPath = fetchDataFileLazy(testName + QDir::separator() +
-                                         prefix + QDir::separator() + filename);
+    const QString standardPath =
+        testName + QDir::separator() +
+        prefix + QDir::separator() + filename;
 
-    if (fullPath.isEmpty()) {
+    QString fullPath = fetchDataFileLazy(standardPath, externalTest);
+
+    if (fullPath.isEmpty() || !QFileInfo(fullPath).exists()) {
         // Try without the testname subdirectory
         fullPath = fetchDataFileLazy(prefix + QDir::separator() +
-                                     filename);
+                                     filename,
+                                     externalTest);
     }
 
-    if (fullPath.isEmpty()) {
+    if (fullPath.isEmpty() || !QFileInfo(fullPath).exists()) {
         // Try without the prefix subdirectory
         fullPath = fetchDataFileLazy(testName + QDir::separator() +
-                                     filename);
+                                     filename,
+                                     externalTest);
     }
 
+    bool canSkipExternalTest = fullPath.isEmpty() && externalTest;
+
     QImage ref(fullPath);
 
     bool valid = true;
     QPoint t;
-    if(!compareQImages(t, image, ref)) {
-        qDebug() << "--- Wrong image:" << name;
-        valid = false;
+    if(!compareQImages(t, image, ref, fuzzy, fuzzyAlpha, maxNumFailingPixels)) {
+        bool saveStandardResults = true;
+
+        if (canSkipExternalTest) {
+            static QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
+            static QString writeUnittestsVar = "KRITA_WRITE_UNITTESTS";
+
+            int writeUnittests = env.value(writeUnittestsVar, "0").toInt();
+            if (writeUnittests) {
+                QString path = fetchExternalDataFileName(standardPath);
+
+                QFileInfo pathInfo(path);
+                QDir directory;
+                directory.mkpath(pathInfo.path());
+
+                qDebug() << "--- Saving reference image:" << name << path;
+                image.save(path);
+                saveStandardResults = false;
+
+            } else {
+                qDebug() << "--- External image not found. Skipping..." << name;
+            }
+        } else {
+            qDebug() << "--- Wrong image:" << name;
+            valid = false;
+        }
 
-        image.save(QString(FILES_OUTPUT_DIR) + QDir::separator() + filename);
-        ref.save(QString(FILES_OUTPUT_DIR) + QDir::separator() + dumpName);
+        if (saveStandardResults) {
+            image.save(QString(FILES_OUTPUT_DIR) + QDir::separator() + filename);
+            ref.save(QString(FILES_OUTPUT_DIR) + QDir::separator() + dumpName);
+        }
     }
 
     return valid;
 }
 
+inline bool checkQImage(const QImage &image, const QString &testName,
+                        const QString &prefix, const QString &name,
+                        int fuzzy = 0, int fuzzyAlpha = -1, int maxNumFailingPixels = 0)
+{
+    return checkQImageImpl(false, image, testName,
+                           prefix, name,
+                           fuzzy, fuzzyAlpha, maxNumFailingPixels);
+}
+
+inline bool checkQImageExternal(const QImage &image, const QString &testName,
+                                const QString &prefix, const QString &name,
+                                int fuzzy = 0, int fuzzyAlpha = -1, int maxNumFailingPixels = 0)
+{
+    return checkQImageImpl(true, image, testName,
+                           prefix, name,
+                           fuzzy, fuzzyAlpha, maxNumFailingPixels);
+}
+
 #endif
 
 inline quint8 alphaDevicePixel(KisPaintDeviceSP dev, qint32 x, qint32 y)


More information about the kimageshop mailing list