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 }