diff --git a/lib/documentview/abstractimageview.h b/lib/documentview/abstractimageview.h --- a/lib/documentview/abstractimageview.h +++ b/lib/documentview/abstractimageview.h @@ -75,12 +75,22 @@ QSizeF documentSize() const; + /** + * Returns the size of the loaded document in device independent pixels. + */ + QSizeF dipDocumentSize() const; + + /* + * The size of the image that is currently visible, + * in device independent pixels. + */ QSizeF visibleImageSize() const; /** * If the image is smaller than the view, imageOffset is the distance from * the topleft corner of the view to the topleft corner of the image. * Neither x nor y can be negative. + * This is in device independent pixels. */ QPointF imageOffset() const; @@ -91,6 +101,8 @@ QPointF scrollPos() const; void setScrollPos(const QPointF& pos); + qreal devicePixelRatio() const; + QPointF mapToView(const QPointF& imagePos) const; QPoint mapToView(const QPoint& imagePos) const; QRectF mapToView(const QRectF& imageRect) const; diff --git a/lib/documentview/abstractimageview.cpp b/lib/documentview/abstractimageview.cpp --- a/lib/documentview/abstractimageview.cpp +++ b/lib/documentview/abstractimageview.cpp @@ -32,6 +32,7 @@ #include #include #include +#include namespace Gwenview { @@ -63,7 +64,7 @@ void adjustImageOffset(Verbosity verbosity = Notify) { - QSizeF zoomedDocSize = q->documentSize() * mZoom; + QSizeF zoomedDocSize = q->dipDocumentSize() * mZoom; QSizeF viewSize = q->boundingRect().size(); QPointF offset( qMax((viewSize.width() - zoomedDocSize.width()) / 2, qreal(0.)), @@ -88,7 +89,7 @@ mScrollPos = _newPos; return; } - QSizeF zoomedDocSize = q->documentSize() * mZoom; + QSizeF zoomedDocSize = q->dipDocumentSize() * mZoom; QSizeF viewSize = q->boundingRect().size(); QPointF newPos( qBound(qreal(0.), _newPos.x(), zoomedDocSize.width() - viewSize.width()), @@ -123,6 +124,7 @@ const QColor light = QColor(192, 192, 192); painter.fillRect(0, 0, 16, 16, light); painter.fillRect(16, 16, 16, 16, light); + pix.setDevicePixelRatio(q->devicePixelRatio()); return pix; } @@ -185,6 +187,11 @@ return d->mDocument ? d->mDocument->size() : QSizeF(); } +QSizeF AbstractImageView::dipDocumentSize() const +{ + return d->mDocument ? d->mDocument->size() / devicePixelRatio(): QSizeF(); +} + qreal AbstractImageView::zoom() const { return d->mZoom; @@ -335,7 +342,7 @@ qreal AbstractImageView::computeZoomToFit() const { - QSizeF docSize = documentSize(); + QSizeF docSize = dipDocumentSize(); if (docSize.isEmpty()) { return 1; } @@ -351,7 +358,7 @@ qreal AbstractImageView::computeZoomToFill() const { - QSizeF docSize = documentSize(); + QSizeF docSize = dipDocumentSize(); if (docSize.isEmpty()) { return 1; } @@ -483,7 +490,7 @@ d->setScrollPos(QPointF(d->mScrollPos.x(), 0)); return; case Qt::Key_End: - d->setScrollPos(QPointF(d->mScrollPos.x(), documentSize().height() * zoom())); + d->setScrollPos(QPointF(d->mScrollPos.x(), dipDocumentSize().height() * zoom())); return; default: return; @@ -523,9 +530,14 @@ d->setScrollPos(pos); } +qreal AbstractImageView::devicePixelRatio() const +{ + return qApp->devicePixelRatio(); +} + QPointF AbstractImageView::mapToView(const QPointF& imagePos) const { - return imagePos * d->mZoom + d->mImageOffset - d->mScrollPos; + return imagePos / devicePixelRatio() * d->mZoom + d->mImageOffset - d->mScrollPos; } QPoint AbstractImageView::mapToView(const QPoint& imagePos) const @@ -537,21 +549,21 @@ { return QRectF( mapToView(imageRect.topLeft()), - imageRect.size() * zoom() + imageRect.size() * zoom() / devicePixelRatio() ); } QRect AbstractImageView::mapToView(const QRect& imageRect) const { return QRect( mapToView(imageRect.topLeft()), - imageRect.size() * zoom() + imageRect.size() * zoom() / devicePixelRatio() ); } QPointF AbstractImageView::mapToImage(const QPointF& viewPos) const { - return (viewPos - d->mImageOffset + d->mScrollPos) / d->mZoom; + return (viewPos - d->mImageOffset + d->mScrollPos) / d->mZoom * devicePixelRatio(); } QPoint AbstractImageView::mapToImage(const QPoint& viewPos) const @@ -563,15 +575,15 @@ { return QRectF( mapToImage(viewRect.topLeft()), - viewRect.size() / zoom() + viewRect.size() / zoom() * devicePixelRatio() ); } QRect AbstractImageView::mapToImage(const QRect& viewRect) const { return QRect( mapToImage(viewRect.topLeft()), - viewRect.size() / zoom() + viewRect.size() / zoom() * devicePixelRatio() ); } @@ -601,7 +613,7 @@ if (!document()) { return QSizeF(); } - QSizeF size = documentSize() * zoom(); + QSizeF size = dipDocumentSize() * zoom(); return size.boundedTo(boundingRect().size()); } diff --git a/lib/documentview/birdeyeview.cpp b/lib/documentview/birdeyeview.cpp --- a/lib/documentview/birdeyeview.cpp +++ b/lib/documentview/birdeyeview.cpp @@ -138,7 +138,7 @@ if (!d->mDocView->canZoom() || d->mDocView->zoomToFit()) { return; } - QSizeF size = d->mDocView->document()->size(); + QSizeF size = d->mDocView->document()->size() / qApp->devicePixelRatio(); size.scale(MIN_SIZE, MIN_SIZE, Qt::KeepAspectRatioByExpanding); QRectF docViewRect = d->mDocView->boundingRect(); int maxBevHeight = docViewRect.height() - 2 * VIEW_OFFSET; @@ -163,7 +163,7 @@ void BirdEyeView::adjustVisibleRect() { - QSizeF docSize = d->mDocView->document()->size(); + QSizeF docSize = d->mDocView->document()->size() / qApp->devicePixelRatio(); qreal viewZoom = d->mDocView->zoom(); qreal bevZoom; if (docSize.height() > docSize.width()) { diff --git a/lib/documentview/documentview.cpp b/lib/documentview/documentview.cpp --- a/lib/documentview/documentview.cpp +++ b/lib/documentview/documentview.cpp @@ -43,6 +43,7 @@ #include #include #include +#include // KDE #include @@ -472,7 +473,10 @@ // This is important for fade effects, where we don't want any background layers visible during the fade. d->mOpacityEffect = new QGraphicsOpacityEffect(this); d->mOpacityEffect->setOpacity(0); - setGraphicsEffect(d->mOpacityEffect); + + // QTBUG-74963. QGraphicsOpacityEffect cause painting an image as non-highdpi. + if (qFuzzyCompare(qApp->devicePixelRatio(), 1.0) || QLibraryInfo::version() >= QVersionNumber(5, 12, 4)) + setGraphicsEffect(d->mOpacityEffect); scene->addItem(this); diff --git a/lib/documentview/rasterimageview.cpp b/lib/documentview/rasterimageview.cpp --- a/lib/documentview/rasterimageview.cpp +++ b/lib/documentview/rasterimageview.cpp @@ -26,6 +26,7 @@ #include #include #include +#include // KDE @@ -35,6 +36,7 @@ #include #include #include +#include namespace Gwenview @@ -139,17 +141,19 @@ void resizeBuffer() { + const auto dpr = q->devicePixelRatio(); QSize size = q->visibleImageSize().toSize(); - if (size == mCurrentBuffer.size()) { + if (size * dpr == mCurrentBuffer.size()) { return; } if (!size.isValid()) { mAlternateBuffer = QPixmap(); mCurrentBuffer = QPixmap(); return; } - mAlternateBuffer = QPixmap(size); + mAlternateBuffer = QPixmap(size * dpr); + mAlternateBuffer.setDevicePixelRatio(dpr); mAlternateBuffer.fill(Qt::transparent); { QPainter painter(&mAlternateBuffer); @@ -384,29 +388,32 @@ { if (d->mAlternateBuffer.size() != d->mCurrentBuffer.size()) { d->mAlternateBuffer = QPixmap(d->mCurrentBuffer.size()); + d->mAlternateBuffer.setDevicePixelRatio(d->mCurrentBuffer.devicePixelRatio()); } d->mAlternateBuffer.fill(Qt::transparent); QPainter painter(&d->mAlternateBuffer); painter.drawPixmap(-delta, d->mCurrentBuffer); } qSwap(d->mCurrentBuffer, d->mAlternateBuffer); // Scale missing parts - QRegion bufferRegion = QRegion(d->mCurrentBuffer.rect().translated(scrollPos().toPoint())); + QRegion bufferRegion = QRect(scrollPos().toPoint(), d->mCurrentBuffer.size() / devicePixelRatio()); QRegion updateRegion = bufferRegion - bufferRegion.translated(-delta.toPoint()); updateBuffer(updateRegion); update(); } void RasterImageView::paint(QPainter* painter, const QStyleOptionGraphicsItem* /*option*/, QWidget* /*widget*/) { + d->mCurrentBuffer.setDevicePixelRatio(devicePixelRatio()); + QPointF topLeft = imageOffset(); if (zoomToFit()) { // In zoomToFit mode, scale crudely the buffer to fit the screen. This // provide an approximate rendered which will be replaced when the scheduled // proper scale is ready. // Round point and size independently, to keep consistency with the below (non zoomToFit) painting - const QRect rect = QRect(topLeft.toPoint(), (documentSize() * zoom()).toSize()); + const QRect rect = QRect(topLeft.toPoint(), (dipDocumentSize() * zoom()).toSize()); painter->drawPixmap(rect, d->mCurrentBuffer); } else { painter->drawPixmap(topLeft.toPoint(), d->mCurrentBuffer); diff --git a/lib/imagescaler.cpp b/lib/imagescaler.cpp --- a/lib/imagescaler.cpp +++ b/lib/imagescaler.cpp @@ -23,6 +23,7 @@ #include #include #include +#include // KDE @@ -45,6 +46,19 @@ // Amount of pixels to keep so that smooth scale is correct static const int SMOOTH_MARGIN = 3; +static inline QRectF scaledRect(const QRectF& rect, qreal factor) +{ + return QRectF(rect.x() * factor, + rect.y() * factor, + rect.width() * factor, + rect.height() * factor); +} + +static inline QRect scaledRect(const QRect& rect, qreal factor) +{ + return scaledRect(QRectF(rect), factor).toAlignedRect(); +} + struct ImageScalerPrivate { Qt::TransformationMode mTransformationMode; @@ -126,9 +140,15 @@ void ImageScaler::scaleRect(const QRect& rect) { + const qreal dpr = qApp->devicePixelRatio(); + + // variables prefixed with dp are in device pixels + const QRect dpRect = Gwenview::scaledRect(rect, dpr); + const qreal REAL_DELTA = 0.001; if (qAbs(d->mZoom - 1.0) < REAL_DELTA) { - QImage tmp = d->mDocument->image().copy(rect); + QImage tmp = d->mDocument->image().copy(dpRect); + tmp.setDevicePixelRatio(dpr); emit scaledRect(rect.left(), rect.top(), tmp); return; } @@ -144,14 +164,12 @@ image = d->mDocument->image(); zoom = d->mZoom; } + const QRect imageRect = Gwenview::scaledRect(image.rect(), 1.0 / dpr); + // If rect contains "half" pixels, make sure sourceRect includes them - QRectF sourceRectF( - rect.left() / zoom, - rect.top() / zoom, - rect.width() / zoom, - rect.height() / zoom); + QRectF sourceRectF = Gwenview::scaledRect(QRectF(rect), 1.0 / zoom); - sourceRectF = sourceRectF.intersected(image.rect()); + sourceRectF = sourceRectF.intersected(imageRect); QRect sourceRect = PaintUtils::containingRect(sourceRectF); if (sourceRect.isEmpty()) { return; @@ -165,8 +183,8 @@ if (needsSmoothMargins) { sourceLeftMargin = qMin(sourceRect.left(), SMOOTH_MARGIN); sourceTopMargin = qMin(sourceRect.top(), SMOOTH_MARGIN); - sourceRightMargin = qMin(image.rect().right() - sourceRect.right(), SMOOTH_MARGIN); - sourceBottomMargin = qMin(image.rect().bottom() - sourceRect.bottom(), SMOOTH_MARGIN); + sourceRightMargin = qMin(imageRect.right() - sourceRect.right(), SMOOTH_MARGIN); + sourceBottomMargin = qMin(imageRect.bottom() - sourceRect.bottom(), SMOOTH_MARGIN); sourceRect.adjust( -sourceLeftMargin, -sourceTopMargin, @@ -182,30 +200,28 @@ } // destRect is almost like rect, but it contains only "full" pixels - QRectF destRectF = QRectF( - sourceRect.left() * zoom, - sourceRect.top() * zoom, - sourceRect.width() * zoom, - sourceRect.height() * zoom - ); - QRect destRect = PaintUtils::containingRect(destRectF); + QRect destRect = Gwenview::scaledRect(sourceRect, zoom); + + QRect dpSourceRect = Gwenview::scaledRect(sourceRect, dpr); + QRect dpDestRect = Gwenview::scaledRect(dpSourceRect, zoom); QImage tmp; - tmp = image.copy(sourceRect); + tmp = image.copy(dpSourceRect); tmp = tmp.scaled( - destRect.width(), - destRect.height(), + dpDestRect.width(), + dpDestRect.height(), Qt::IgnoreAspectRatio, // Do not use KeepAspectRatio, it can lead to skipped rows or columns d->mTransformationMode); if (needsSmoothMargins) { tmp = tmp.copy( - destLeftMargin, destTopMargin, - destRect.width() - (destLeftMargin + destRightMargin), - destRect.height() - (destTopMargin + destBottomMargin) + destLeftMargin * dpr, destTopMargin * dpr, + dpDestRect.width() - (destLeftMargin + destRightMargin) * dpr, + dpDestRect.height() - (destTopMargin + destBottomMargin) * dpr ); } + tmp.setDevicePixelRatio(dpr); emit scaledRect(destRect.left() + destLeftMargin, destRect.top() + destTopMargin, tmp); }