File indexing completed on 2024-05-12 15:56:12

0001 /*
0002  *  SPDX-FileCopyrightText: 2013 Dmitry Kazakov <dimula73@gmail.com>
0003  *
0004  *  SPDX-License-Identifier: GPL-2.0-or-later
0005  */
0006 
0007 #include "kis_qimage_pyramid.h"
0008 
0009 #include <limits>
0010 #include <QPainter>
0011 #include <kis_debug.h>
0012 
0013 #define MIPMAP_SIZE_THRESHOLD 512
0014 #define MAX_MIPMAP_SCALE 8.0
0015 
0016 #define QPAINTER_WORKAROUND_BORDER 1
0017 
0018 
0019 KisQImagePyramid::KisQImagePyramid(const QImage &baseImage, bool useSmoothingForEnlarging)
0020 {
0021     KIS_SAFE_ASSERT_RECOVER_RETURN(!baseImage.isNull());
0022 
0023     m_originalSize = baseImage.size();
0024 
0025 
0026     qreal scale = MAX_MIPMAP_SCALE;
0027 
0028     while (scale > 1.0) {
0029         QSize scaledSize = m_originalSize * scale;
0030 
0031         if (scaledSize.width() <= MIPMAP_SIZE_THRESHOLD ||
0032                 scaledSize.height() <= MIPMAP_SIZE_THRESHOLD) {
0033 
0034             if (m_levels.isEmpty()) {
0035                 m_baseScale = scale;
0036             }
0037 
0038             if (useSmoothingForEnlarging) {
0039                 appendPyramidLevel(baseImage.scaled(scaledSize,  Qt::IgnoreAspectRatio, Qt::SmoothTransformation));
0040             } else {
0041                 appendPyramidLevel(baseImage.scaled(scaledSize,  Qt::IgnoreAspectRatio, Qt::FastTransformation));
0042             }
0043         }
0044 
0045         scale *= 0.5;
0046     }
0047 
0048     if (m_levels.isEmpty()) {
0049         m_baseScale = 1.0;
0050     }
0051     appendPyramidLevel(baseImage);
0052 
0053     scale = 0.5;
0054     while (true) {
0055         QSize scaledSize = m_originalSize * scale;
0056 
0057         if (scaledSize.width() == 0 ||
0058                 scaledSize.height() == 0) break;
0059 
0060         appendPyramidLevel(baseImage.scaled(scaledSize,  Qt::IgnoreAspectRatio, Qt::SmoothTransformation));
0061 
0062         scale *= 0.5;
0063     }
0064 }
0065 
0066 KisQImagePyramid::~KisQImagePyramid()
0067 {
0068 }
0069 
0070 int KisQImagePyramid::findNearestLevel(qreal scale, qreal *baseScale) const
0071 {
0072     const qreal scale_epsilon = 1e-6;
0073 
0074     qreal levelScale = m_baseScale;
0075     int level = 0;
0076     int lastLevel = m_levels.size() - 1;
0077 
0078 
0079     while ((0.5 * levelScale > scale ||
0080             qAbs(0.5 * levelScale - scale) < scale_epsilon) &&
0081             level < lastLevel) {
0082 
0083         levelScale *= 0.5;
0084         level++;
0085     }
0086 
0087     *baseScale = levelScale;
0088     return level;
0089 }
0090 
0091 inline QRect roundRect(const QRectF &rc)
0092 {
0093     /**
0094      * This is an analog of toAlignedRect() with the only difference
0095      * that it ensures the rect position will never be below zero.
0096      *
0097      * Warning: be *very* careful with using bottom()/right() values
0098      *          of a pure QRect (we don't use it here for the dangers
0099      *          it can lead to).
0100      */
0101 
0102     QRectF rect(rc);
0103 
0104     KIS_SAFE_ASSERT_RECOVER_NOOP(rect.x() > -0.000001);
0105     KIS_SAFE_ASSERT_RECOVER_NOOP(rect.y() > -0.000001);
0106 
0107     if (rect.x() < 0.000001) {
0108         rect.setLeft(0.0);
0109     }
0110 
0111     if (rect.y() < 0.000001) {
0112         rect.setTop(0.0);
0113     }
0114 
0115     return rect.toAlignedRect();
0116 }
0117 
0118 QTransform baseBrushTransform(KisDabShape const& shape,
0119                               qreal subPixelX, qreal subPixelY,
0120                               const QRectF &baseBounds)
0121 {
0122     QTransform transform;
0123     transform.scale(shape.scaleX(), shape.scaleY());
0124 
0125     if (!qFuzzyCompare(shape.rotation(), 0) && !qIsNaN(shape.rotation())) {
0126         transform = transform * QTransform().rotateRadians(shape.rotation());
0127         QRectF rotatedBounds = transform.mapRect(baseBounds);
0128         transform = transform * QTransform::fromTranslate(-rotatedBounds.x(), -rotatedBounds.y());
0129     }
0130 
0131     return transform * QTransform::fromTranslate(subPixelX, subPixelY);
0132 }
0133 
0134 void KisQImagePyramid::calculateParams(KisDabShape const& shape,
0135                                        qreal subPixelX, qreal subPixelY,
0136                                        const QSize &originalSize,
0137                                        QTransform *outputTransform, QSize *outputSize)
0138 {
0139     calculateParams(shape,
0140                     subPixelX, subPixelY,
0141                     originalSize, 1.0, originalSize,
0142                     outputTransform, outputSize);
0143 }
0144 
0145 void KisQImagePyramid::calculateParams(KisDabShape shape,
0146                                        qreal subPixelX, qreal subPixelY,
0147                                        const QSize &originalSize,
0148                                        qreal baseScale, const QSize &baseSize,
0149                                        QTransform *outputTransform, QSize *outputSize)
0150 {
0151     Q_UNUSED(baseScale);
0152 
0153     QRectF originalBounds = QRectF(QPointF(), originalSize);
0154     QTransform originalTransform = baseBrushTransform(shape, subPixelX, subPixelY, originalBounds);
0155 
0156     qreal realBaseScaleX = qreal(baseSize.width()) / originalSize.width();
0157     qreal realBaseScaleY = qreal(baseSize.height()) / originalSize.height();
0158     qreal scaleX = shape.scaleX() / realBaseScaleX;
0159     qreal scaleY = shape.scaleY() / realBaseScaleY;
0160     shape = KisDabShape(scaleX, scaleY/scaleX, shape.rotation());
0161 
0162     QRectF baseBounds = QRectF(QPointF(), baseSize);
0163     QTransform transform = baseBrushTransform(shape, subPixelX, subPixelY, baseBounds);
0164     QRectF mappedRect = originalTransform.mapRect(originalBounds);
0165 
0166     // Set up a 0,0,1,1 size and identity transform in case the transform fails to
0167     // produce a usable result.
0168     int width = 1;
0169     int height = 1;
0170     *outputTransform = QTransform();
0171 
0172     if (mappedRect.isValid()) {
0173         QRect expectedDstRect = roundRect(mappedRect);
0174 
0175 #if 0 // Only enable when debugging; users shouldn't see this warning
0176         {
0177             QRect testingRect = roundRect(transform.mapRect(baseBounds));
0178             if (testingRect != expectedDstRect) {
0179                 warnKrita << "WARNING: expected and real dab rects do not coincide!";
0180                 warnKrita << "         expected rect:" << expectedDstRect;
0181                 warnKrita << "         real rect:    " << testingRect;
0182             }
0183         }
0184 #endif
0185         KIS_SAFE_ASSERT_RECOVER_NOOP(expectedDstRect.x() >= 0);
0186         KIS_SAFE_ASSERT_RECOVER_NOOP(expectedDstRect.y() >= 0);
0187 
0188         width = expectedDstRect.x() + expectedDstRect.width();
0189         height = expectedDstRect.y() + expectedDstRect.height();
0190 
0191         // we should not return invalid image, so adjust the image to be
0192         // at least 1 px in size.
0193         width = qMax(1, width);
0194         height = qMax(1, height);
0195     }
0196     else {
0197 #if 0 // Only enable when debugging; users shouldn't see this warning
0198         qWarning() << "Brush transform generated an invalid rectangle!"
0199             << ppVar(shape.scaleX()) << ppVar(shape.scaleY()) << ppVar(shape.rotation())
0200             << ppVar(subPixelX) << ppVar(subPixelY)
0201             << ppVar(originalSize)
0202             << ppVar(baseScale)
0203             << ppVar(baseSize)
0204             << ppVar(baseBounds)
0205             << ppVar(mappedRect);
0206 #endif
0207     }
0208 
0209     *outputTransform = transform;
0210     *outputSize = QSize(width, height);
0211 }
0212 
0213 QSize KisQImagePyramid::imageSize(const QSize &originalSize,
0214                                   KisDabShape const& shape,
0215                                   qreal subPixelX, qreal subPixelY)
0216 {
0217     QTransform transform;
0218     QSize dstSize;
0219 
0220     calculateParams(shape, subPixelX, subPixelY,
0221                     originalSize,
0222                     &transform, &dstSize);
0223 
0224     return dstSize;
0225 }
0226 
0227 QSizeF KisQImagePyramid::characteristicSize(const QSize &originalSize,
0228                                             KisDabShape const& shape)
0229 {
0230     QRectF originalRect(QPointF(), originalSize);
0231     QTransform transform = baseBrushTransform(shape,
0232                                               0.0, 0.0,
0233                                               originalRect);
0234 
0235     return transform.mapRect(originalRect).size();
0236 }
0237 
0238 void KisQImagePyramid::appendPyramidLevel(const QImage &image)
0239 {
0240     /**
0241      * QPainter has a bug: when doing a transformation it decides that
0242      * all the pixels outside of the image (source rect) are equal to
0243      * the border pixels (CLAMP in terms of openGL). This means that
0244      * there will be no smooth scaling on the border of the image when
0245      * it is rotated.  To workaround this bug we need to add one pixel
0246      * wide border to the image, so that it transforms smoothly.
0247      *
0248      * See a unittest in: KisGbrBrushTest::testQPainterTransformationBorder
0249      */
0250     
0251 QSize levelSize = image.size();
0252     QImage tmp = image.convertToFormat(QImage::Format_ARGB32);
0253     tmp = tmp.copy(-QPAINTER_WORKAROUND_BORDER,
0254                    -QPAINTER_WORKAROUND_BORDER,
0255                    image.width() + 2 * QPAINTER_WORKAROUND_BORDER,
0256                    image.height() + 2 * QPAINTER_WORKAROUND_BORDER);
0257     m_levels.append(PyramidLevel(tmp, levelSize));
0258 }
0259 
0260 QImage KisQImagePyramid::createImage(KisDabShape const& shape,
0261                                      qreal subPixelX, qreal subPixelY) const
0262 {
0263     if (m_levels.isEmpty()) return QImage();
0264 
0265     qreal baseScale = -1.0;
0266     int level = findNearestLevel(shape.scale(), &baseScale);
0267 
0268     const QImage &srcImage = m_levels[level].image;
0269 
0270     QTransform transform;
0271     QSize dstSize;
0272 
0273     calculateParams(shape, subPixelX, subPixelY,
0274                     m_originalSize, baseScale, m_levels[level].size,
0275                     &transform, &dstSize);
0276 
0277     if (transform.isIdentity() &&
0278             srcImage.format() == QImage::Format_ARGB32) {
0279 
0280         return srcImage.copy(QPAINTER_WORKAROUND_BORDER,
0281                              QPAINTER_WORKAROUND_BORDER,
0282                              srcImage.width() - 2 * QPAINTER_WORKAROUND_BORDER,
0283                              srcImage.height() - 2 * QPAINTER_WORKAROUND_BORDER);
0284     }
0285 
0286     QImage dstImage(dstSize, QImage::Format_ARGB32);
0287     dstImage.fill(0);
0288 
0289 
0290     /**
0291      * QPainter has one more bug: when a QTransform is TxTranslate, it
0292      * does wrong sampling (probably, Nearest Neighbour) even though
0293      * we tell it directly that we need SmoothPixmapTransform.
0294      *
0295      * So here is a workaround: we set a negligible scale to convince
0296      * Qt we use a non-only-translating transform.
0297      */
0298     while (transform.type() == QTransform::TxTranslate) {
0299         const qreal scale = transform.m11();
0300         const qreal fakeScale = scale - 10 * std::numeric_limits<qreal>::epsilon();
0301         transform *= QTransform::fromScale(fakeScale, fakeScale);
0302     }
0303 
0304     QPainter gc(&dstImage);
0305     gc.setTransform(
0306         QTransform::fromTranslate(-QPAINTER_WORKAROUND_BORDER,
0307                                   -QPAINTER_WORKAROUND_BORDER) * transform);
0308     gc.setRenderHints(QPainter::SmoothPixmapTransform);
0309     gc.drawImage(QPointF(), srcImage);
0310     gc.end();
0311 
0312     return dstImage;
0313 }
0314 
0315 QImage KisQImagePyramid::getClosest(QTransform transform, qreal *scale) const
0316 {
0317     if (m_levels.isEmpty()) return QImage();
0318 
0319     // Estimate scale
0320     QSizeF transformedUnitSquare = transform.mapRect(QRectF(0, 0, 1, 1)).size();
0321     qreal x = qAbs(transformedUnitSquare.width());
0322     qreal y = qAbs(transformedUnitSquare.height());
0323     qreal estimatedScale = (x > y) ? transformedUnitSquare.width() : transformedUnitSquare.height();
0324 
0325     int level = findNearestLevel(estimatedScale, scale);
0326     return m_levels[level].image;
0327 }
0328 
0329 QImage KisQImagePyramid::getClosestWithoutWorkaroundBorder(QTransform transform, qreal *scale) const
0330 {
0331     QImage image = getClosest(transform, scale);
0332     return image.copy(QPAINTER_WORKAROUND_BORDER,
0333                QPAINTER_WORKAROUND_BORDER,
0334                image.width() - 2 * QPAINTER_WORKAROUND_BORDER,
0335                image.height() - 2 * QPAINTER_WORKAROUND_BORDER);
0336 }