kis_qimage_pyramid.cpp 11 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
/*
 *  Copyright (c) 2013 Dmitry Kazakov <dimula73@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_qimage_pyramid.h"

21
#include <limits>
22 23 24 25 26 27
#include <QPainter>
#include <kis_debug.h>

#define MIPMAP_SIZE_THRESHOLD 512
#define MAX_MIPMAP_SCALE 8.0

28 29 30
#define QPAINTER_WORKAROUND_BORDER 1


31 32
KisQImagePyramid::KisQImagePyramid(const QImage &baseImage)
{
33
    KIS_SAFE_ASSERT_RECOVER_RETURN(!baseImage.isNull());
34

35
    m_originalSize = baseImage.size();
36 37 38 39 40


    qreal scale = MAX_MIPMAP_SCALE;

    while (scale > 1.0) {
41
        QSize scaledSize = m_originalSize * scale;
42 43

        if (scaledSize.width() <= MIPMAP_SIZE_THRESHOLD ||
44
                scaledSize.height() <= MIPMAP_SIZE_THRESHOLD) {
45 46 47 48 49

            if (m_levels.isEmpty()) {
                m_baseScale = scale;
            }

50
            appendPyramidLevel(baseImage.scaled(scaledSize,  Qt::IgnoreAspectRatio, Qt::SmoothTransformation));
51 52 53 54 55 56 57 58
        }

        scale *= 0.5;
    }

    if (m_levels.isEmpty()) {
        m_baseScale = 1.0;
    }
59
    appendPyramidLevel(baseImage);
60 61 62

    scale = 0.5;
    while (true) {
63
        QSize scaledSize = m_originalSize * scale;
64 65

        if (scaledSize.width() == 0 ||
66
                scaledSize.height() == 0) break;
67

68
        appendPyramidLevel(baseImage.scaled(scaledSize,  Qt::IgnoreAspectRatio, Qt::SmoothTransformation));
69 70 71 72 73 74 75 76 77

        scale *= 0.5;
    }
}

KisQImagePyramid::~KisQImagePyramid()
{
}

78
int KisQImagePyramid::findNearestLevel(qreal scale, qreal *baseScale) const
79
{
80 81
    const qreal scale_epsilon = 1e-6;

82 83 84 85
    qreal levelScale = m_baseScale;
    int level = 0;
    int lastLevel = m_levels.size() - 1;

86 87 88 89 90

    while ((0.5 * levelScale > scale ||
            qAbs(0.5 * levelScale - scale) < scale_epsilon) &&
            level < lastLevel) {

91 92 93 94 95 96 97 98
        levelScale *= 0.5;
        level++;
    }

    *baseScale = levelScale;
    return level;
}

99 100
inline QRect roundRect(const QRectF &rc)
{
101 102
    /**
     * This is an analog of toAlignedRect() with the only difference
103
     * that it ensures the rect position will never be below zero.
104 105 106 107 108 109
     *
     * Warning: be *very* careful with using bottom()/right() values
     *          of a pure QRect (we don't use it here for the dangers
     *          it can lead to).
     */

110
    QRectF rect(rc);
111

112 113
    KIS_SAFE_ASSERT_RECOVER_NOOP(rect.x() > -0.000001);
    KIS_SAFE_ASSERT_RECOVER_NOOP(rect.y() > -0.000001);
114

115
    if (rect.x() < 0.000001) {
116 117 118
        rect.setLeft(0.0);
    }

119
    if (rect.y() < 0.000001) {
120 121 122 123
        rect.setTop(0.0);
    }

    return rect.toAlignedRect();
124 125
}

126
QTransform baseBrushTransform(KisDabShape const& shape,
127 128
                              qreal subPixelX, qreal subPixelY,
                              const QRectF &baseBounds)
129 130
{
    QTransform transform;
131
    transform.scale(shape.scaleX(), shape.scaleY());
132

133
    if (!qFuzzyCompare(shape.rotation(), 0) && !qIsNaN(shape.rotation())) {
134 135 136
        transform = transform * QTransform().rotateRadians(shape.rotation());
        QRectF rotatedBounds = transform.mapRect(baseBounds);
        transform = transform * QTransform::fromTranslate(-rotatedBounds.x(), -rotatedBounds.y());
137 138
    }

139
    return transform * QTransform::fromTranslate(subPixelX, subPixelY);
140 141
}

142
void KisQImagePyramid::calculateParams(KisDabShape const& shape,
143 144 145 146
                                       qreal subPixelX, qreal subPixelY,
                                       const QSize &originalSize,
                                       QTransform *outputTransform, QSize *outputSize)
{
147
    calculateParams(shape,
148 149 150 151 152
                    subPixelX, subPixelY,
                    originalSize, 1.0, originalSize,
                    outputTransform, outputSize);
}

153
void KisQImagePyramid::calculateParams(KisDabShape shape,
154 155 156 157 158
                                       qreal subPixelX, qreal subPixelY,
                                       const QSize &originalSize,
                                       qreal baseScale, const QSize &baseSize,
                                       QTransform *outputTransform, QSize *outputSize)
{
159 160
    Q_UNUSED(baseScale);

161
    QRectF originalBounds = QRectF(QPointF(), originalSize);
162
    QTransform originalTransform = baseBrushTransform(shape, subPixelX, subPixelY, originalBounds);
163

164 165
    qreal realBaseScaleX = qreal(baseSize.width()) / originalSize.width();
    qreal realBaseScaleY = qreal(baseSize.height()) / originalSize.height();
166 167 168
    qreal scaleX = shape.scaleX() / realBaseScaleX;
    qreal scaleY = shape.scaleY() / realBaseScaleY;
    shape = KisDabShape(scaleX, scaleY/scaleX, shape.rotation());
169 170

    QRectF baseBounds = QRectF(QPointF(), baseSize);
171 172 173 174 175 176 177 178 179 180 181
    QTransform transform = baseBrushTransform(shape, subPixelX, subPixelY, baseBounds);
    QRectF mappedRect = originalTransform.mapRect(originalBounds);

    // Set up a 0,0,1,1 size and identity transform in case the transform fails to
    // produce a usable result.
    int width = 1;
    int height = 1;
    *outputTransform = QTransform();

    if (mappedRect.isValid()) {
        QRect expectedDstRect = roundRect(mappedRect);
182

Boudewijn Rempt's avatar
Boudewijn Rempt committed
183
#if 0 // Only enable when debugging; users shouldn't see this warning
184 185 186 187 188 189 190
        {
            QRect testingRect = roundRect(transform.mapRect(baseBounds));
            if (testingRect != expectedDstRect) {
                warnKrita << "WARNING: expected and real dab rects do not coincide!";
                warnKrita << "         expected rect:" << expectedDstRect;
                warnKrita << "         real rect:    " << testingRect;
            }
191
        }
Boudewijn Rempt's avatar
Boudewijn Rempt committed
192
#endif
193 194
        KIS_SAFE_ASSERT_RECOVER_NOOP(expectedDstRect.x() >= 0);
        KIS_SAFE_ASSERT_RECOVER_NOOP(expectedDstRect.y() >= 0);
195

196 197
        width = expectedDstRect.x() + expectedDstRect.width();
        height = expectedDstRect.y() + expectedDstRect.height();
198

199 200 201 202 203 204 205 206 207 208 209 210 211 212 213
        // we should not return invalid image, so adjust the image to be
        // at least 1 px in size.
        width = qMax(1, width);
        height = qMax(1, height);
    }
    else {
        qWarning() << "Brush transform generated an invalid rectangle!" 
            << ppVar(shape.scaleX()) << ppVar(shape.scaleY()) << ppVar(shape.rotation())
            << ppVar(subPixelX) << ppVar(subPixelY)
            << ppVar(originalSize)
            << ppVar(baseScale)
            << ppVar(baseSize)
            << ppVar(baseBounds)
            << ppVar(mappedRect);
    }
214 215 216 217 218

    *outputTransform = transform;
    *outputSize = QSize(width, height);
}

219
QSize KisQImagePyramid::imageSize(const QSize &originalSize,
220
                                  KisDabShape const& shape,
221 222 223 224 225
                                  qreal subPixelX, qreal subPixelY)
{
    QTransform transform;
    QSize dstSize;

226
    calculateParams(shape, subPixelX, subPixelY,
227
                    originalSize,
228 229 230 231 232
                    &transform, &dstSize);

    return dstSize;
}

233
QSizeF KisQImagePyramid::characteristicSize(const QSize &originalSize,
234
                                            KisDabShape const& shape)
235 236
{
    QRectF originalRect(QPointF(), originalSize);
237
    QTransform transform = baseBrushTransform(shape,
238 239 240 241 242 243
                                              0.0, 0.0,
                                              originalRect);

    return transform.mapRect(originalRect).size();
}

244 245 246 247 248 249 250 251 252 253
void KisQImagePyramid::appendPyramidLevel(const QImage &image)
{
    /**
     * QPainter has a bug: when doing a transformation it decides that
     * all the pixels outside of the image (source rect) are equal to
     * the border pixels (CLAMP in terms of openGL). This means that
     * there will be no smooth scaling on the border of the image when
     * it is rotated.  To workaround this bug we need to add one pixel
     * wide border to the image, so that it transforms smoothly.
     *
254
     * See a unittest in: KisGbrBrushTest::testQPainterTransformationBorder
255
     */
256 257
    
QSize levelSize = image.size();
258 259 260 261 262 263 264 265
    QImage tmp = image.convertToFormat(QImage::Format_ARGB32);
    tmp = tmp.copy(-QPAINTER_WORKAROUND_BORDER,
                   -QPAINTER_WORKAROUND_BORDER,
                   image.width() + 2 * QPAINTER_WORKAROUND_BORDER,
                   image.height() + 2 * QPAINTER_WORKAROUND_BORDER);
    m_levels.append(PyramidLevel(tmp, levelSize));
}

266
QImage KisQImagePyramid::createImage(KisDabShape const& shape,
267
                                     qreal subPixelX, qreal subPixelY) const
268
{
269 270
    if (m_levels.isEmpty()) return QImage();

271
    qreal baseScale = -1.0;
272
    int level = findNearestLevel(shape.scale(), &baseScale);
273

274
    const QImage &srcImage = m_levels[level].image;
275 276 277 278

    QTransform transform;
    QSize dstSize;

279
    calculateParams(shape, subPixelX, subPixelY,
280
                    m_originalSize, baseScale, m_levels[level].size,
281 282
                    &transform, &dstSize);

283
    if (transform.isIdentity() &&
284
            srcImage.format() == QImage::Format_ARGB32) {
285

286 287 288 289
        return srcImage.copy(QPAINTER_WORKAROUND_BORDER,
                             QPAINTER_WORKAROUND_BORDER,
                             srcImage.width() - 2 * QPAINTER_WORKAROUND_BORDER,
                             srcImage.height() - 2 * QPAINTER_WORKAROUND_BORDER);
290 291 292 293 294
    }

    QImage dstImage(dstSize, QImage::Format_ARGB32);
    dstImage.fill(0);

295 296 297 298 299 300 301 302 303 304 305 306 307 308 309

    /**
     * QPainter has one more bug: when a QTransform is TxTranslate, it
     * does wrong sampling (probably, Nearest Neighbour) even though
     * we tell it directly that we need SmoothPixmapTransform.
     *
     * So here is a workaround: we set a negligible scale to convince
     * Qt we use a non-only-translating transform.
     */
    while (transform.type() == QTransform::TxTranslate) {
        const qreal scale = transform.m11();
        const qreal fakeScale = scale - 10 * std::numeric_limits<qreal>::epsilon();
        transform *= QTransform::fromScale(fakeScale, fakeScale);
    }

310
    QPainter gc(&dstImage);
311 312 313
    gc.setTransform(
        QTransform::fromTranslate(-QPAINTER_WORKAROUND_BORDER,
                                  -QPAINTER_WORKAROUND_BORDER) * transform);
314 315 316 317 318 319 320
    gc.setRenderHints(QPainter::SmoothPixmapTransform);
    gc.drawImage(QPointF(), srcImage);
    gc.end();

    return dstImage;
}

321 322 323 324 325 326 327 328 329 330 331 332 333
QImage KisQImagePyramid::getClosest(QTransform transform, qreal *scale) const
{
    if (m_levels.isEmpty()) return QImage();

    // Estimate scale
    QSizeF transformedUnitSquare = transform.mapRect(QRectF(0, 0, 1, 1)).size();
    qreal x = qAbs(transformedUnitSquare.width());
    qreal y = qAbs(transformedUnitSquare.height());
    qreal estimatedScale = (x > y) ? transformedUnitSquare.width() : transformedUnitSquare.height();

    int level = findNearestLevel(estimatedScale, scale);
    return m_levels[level].image;
}