#include "Document/BrushStroke.h" #include "Rendering/AdjustmentSurface.h" #include "Rendering/EditorCanvas.h" #include "RenderFixtures.h" #include "UI/JPEGExportSheet.h" #include "UI/LevelsSheet.h" #include #include // Widgets at device pixel ratio 2: two coordinate systems. namespace { struct Probe : QWidget { std::function body; void paintEvent(QPaintEvent *) override { QPainter painter(this); if (body) body(painter); } }; // A probe at an offset inside a window, both shown. struct Framed { QWidget window; Probe child; Framed() { window.resize(300, 200); child.setParent(&window); child.setGeometry(57, 42, 200, 100); window.show(); if (!QTest::qWaitForWindowExposed(&window)) throw std::runtime_error("the window never showed"); } }; } class HighDpiCanvasTests : public QObject { Q_OBJECT private slots: void theScreenScalesByTwo(); void theVisibleRectIsTheWidgetInItsOwnPoints(); void theSurfaceCoversAnOffsetWidgetInDevicePixels(); void theCanvasDrawsDevicePixels(); void levelsEyedroppersDrawDevicePixels(); void theJPEGPreviewCountsDevicePixels(); void gridDashesAreScreenPoints(); }; void HighDpiCanvasTests::theScreenScalesByTwo() { QWidget widget; QCOMPARE(widget.devicePixelRatio(), 2.0); } void HighDpiCanvasTests::theVisibleRectIsTheWidgetInItsOwnPoints() { Framed framed; std::vector seen; framed.child.body = [&](QPainter &painter) { seen.push_back(BrushRaster::visibleRect(painter)); painter.translate(10, 5); seen.push_back(BrushRaster::visibleRect(painter)); painter.setClipRect(QRectF(10, 10, 50, 20)); seen.push_back(BrushRaster::visibleRect(painter)); }; // Through a grab and through the window's own store alike. framed.child.grab(); framed.child.repaint(); QCOMPARE(int(seen.size()), 6); for (size_t pass = 0; pass < 6; pass += 3) { QCOMPARE(seen[pass], QRectF(0, 0, 200, 100)); QCOMPARE(seen[pass + 1], QRectF(-10, -5, 200, 100)); QCOMPARE(seen[pass + 2], QRectF(10, 10, 50, 20)); } } void HighDpiCanvasTests::theSurfaceCoversAnOffsetWidgetInDevicePixels() { Framed framed; QSize allocated; framed.child.body = [&](QPainter &painter) { AdjustmentSurface::draw(painter, [&](QPainter &surface) { allocated = QSize(surface.device()->width(), surface.device()->height()); surface.fillRect(QRectF(0, 0, 200, 100), Qt::red); }); }; QImage shot = framed.child.grab().toImage().convertToFormat(QImage::Format_ARGB32); QCOMPARE(shot.size(), QSize(400, 200)); QCOMPARE(allocated, QSize(400, 200)); for (const QPoint &point : {QPoint(0, 0), QPoint(399, 0), QPoint(0, 199), QPoint(399, 199), QPoint(200, 100)}) QCOMPARE(shot.pixel(point), qRgb(255, 0, 0)); // Through the window the child keeps its store offset. const QImage whole = framed.window.grab().toImage().convertToFormat(QImage::Format_ARGB32); QCOMPARE(whole.size(), QSize(600, 400)); for (const QPoint &point : {QPoint(114, 84), QPoint(513, 84), QPoint(114, 283), QPoint(513, 283), QPoint(314, 184)}) QCOMPARE(whole.pixel(point), qRgb(255, 0, 0)); QVERIFY(whole.pixel(113, 84) != qRgb(255, 0, 0)); QVERIFY(whole.pixel(514, 283) != qRgb(255, 0, 0)); // A clip bounds the surface, in device pixels too. framed.child.body = [&](QPainter &painter) { painter.fillRect(QRectF(0, 0, 200, 100), Qt::black); painter.setClipRect(QRectF(10, 10, 50, 20)); AdjustmentSurface::draw(painter, [&](QPainter &surface) { allocated = QSize(surface.device()->width(), surface.device()->height()); surface.fillRect(QRectF(0, 0, 200, 100), Qt::red); }); }; shot = framed.child.grab().toImage().convertToFormat(QImage::Format_ARGB32); QCOMPARE(allocated, QSize(100, 40)); QCOMPARE(shot.pixel(20, 20), qRgb(255, 0, 0)); QCOMPARE(shot.pixel(119, 59), qRgb(255, 0, 0)); QCOMPARE(shot.pixel(10, 10), qRgb(0, 0, 0)); QCOMPARE(shot.pixel(120, 60), qRgb(0, 0, 0)); } void HighDpiCanvasTests::theCanvasDrawsDevicePixels() { EditorSession session; session.createDocument(2, 1); QImage edge = BrushRaster::context(2, 1, false); edge.setPixel(0, 0, qRgba(0, 0, 0, 255)); edge.setPixel(1, 0, qRgba(255, 255, 255, 255)); session.insert(ImportedImage(edge, edge, "Edge"), QPointF(1, 0.5)); session.setShowsTransformControls(false); QWidget window; auto *canvas = new CanvasView(session, &window); QPalette dark = canvas->palette(); dark.setColor(QPalette::Base, QColor(0x1b, 0x1b, 0x1b)); canvas->setPalette(dark); window.resize(300, 200); canvas->setGeometry(0, 0, 40, 20); window.show(); QVERIFY(QTest::qWaitForWindowExposed(&window)); QTRY_COMPARE(session.viewport.backingScale, 2.0); QCOMPARE(session.viewport.viewSize, QSizeF(40, 20)); session.zoom(4); QCOMPARE(session.viewport.pointsPerPixel(), 2.0); const QImage shot = canvas->grab().toImage().convertToFormat(QImage::Format_ARGB32); QCOMPARE(shot.size(), QSize(80, 40)); // The two document pixels span eight device pixels, hard-edged. QCOMPARE(qRed(shot.pixel(37, 19)), 0); QCOMPARE(qRed(shot.pixel(39, 19)), 0); QCOMPARE(qRed(shot.pixel(40, 19)), 255); QCOMPARE(qRed(shot.pixel(42, 19)), 255); QCOMPARE(qRed(shot.pixel(2, 2)), 27); } void HighDpiCanvasTests::levelsEyedroppersDrawDevicePixels() { EditorSession session; session.createDocument(2, 1); QImage red(2, 1, QImage::Format_RGBA8888_Premultiplied); red.fill(Qt::red); session.insert(ImportedImage(red, red, QStringLiteral("Red"))); session.beginLevels(); LevelsSheet sheet(session); sheet.show(); QVERIFY(QTest::qWaitForWindowExposed(&sheet)); // Fourteen points: the glyph spans all 28 pixels. for (QPushButton *button : sheet.findChildren()) { if (button->text() == QStringLiteral("Black")) { const QImage drawn = button->icon().pixmap(QSize(14, 14)).toImage(); QCOMPARE(drawn.size(), QSize(28, 28)); int right = 0; for (int y = 0; y < 28; ++y) { for (int x = 16; x < 28; ++x) right += qAlpha(drawn.pixel(x, y)) > 0; } QVERIFY(right > 20); } } } void HighDpiCanvasTests::theJPEGPreviewCountsDevicePixels() { QImage image(1200, 800, QImage::Format_RGBA8888_Premultiplied); for (int y = 0; y < 800; ++y) for (int x = 0; x < 1200; ++x) image.setPixelColor(x, y, QColor((x * 7 + y) % 256, (x + y * 3) % 256, (x * y) % 256)); JPEGPreview preview(QSize(1200, 800), [] {}, nullptr); preview.setImage(image, false); preview.show(); QVERIFY(QTest::qWaitForWindowExposed(&preview)); // 600 by 400 points: Fit 0.825, averaged per device pixel. QCOMPARE(preview.shownZoom(), 0.825); QImage fitted = preview.viewport()->grab().toImage().convertToFormat(QImage::Format_RGB32); fitted.setDevicePixelRatio(1); QCOMPARE(fitted.size(), QSize(1120, 660)); QCOMPARE(fitted.copy(65, 0, 990, 660), image.scaled(990, 660, Qt::IgnoreAspectRatio, Qt::SmoothTransformation).convertToFormat(QImage::Format_RGB32)); // At 100% an image pixel is a device pixel. preview.setZoom(1); QCoreApplication::processEvents(); QCOMPARE(preview.horizontalScrollBar()->maximum(), int(std::ceil(600 - preview.viewport()->width()))); } void HighDpiCanvasTests::gridDashesAreScreenPoints() { EditorSession session; session.createDocument(100, 100); QWidget window; auto *canvas = new CanvasView(session, &window); window.resize(100, 100); canvas->setGeometry(0, 0, 100, 100); window.show(); QVERIFY(QTest::qWaitForWindowExposed(&window)); QTRY_COMPARE(session.viewport.backingScale, 2.0); session.zoom(1); const QImage bare = canvas->grab().toImage(); session.setShowsGrid(true); session.setLayoutGrid(LayoutGrid(50, 1)); GridAppearance dashed; dashed.preset = GridAppearance::Preset::black; dashed.opacity = 100; dashed.style = GridAppearance::Style::dashedLines; session.setGridAppearance(dashed); canvas->synchronizeDisplay(); const QImage drawn = canvas->grab().toImage(); // Four points on is eight device rows, not four. const int column = int(std::round(session.viewport.viewPoint(QPointF(50, 0), QSizeF(100, 100)).x() * 2)); int run = 0, longest = 0; for (int y = 4; y < 180; ++y) { const bool inked = std::abs(qGray(drawn.pixel(column, y)) - qGray(bare.pixel(column, y))) + std::abs(qGray(drawn.pixel(column - 1, y)) - qGray(bare.pixel(column - 1, y))) > 20; run = inked ? run + 1 : 0; longest = std::max(longest, run); } QVERIFY2(longest >= 7 && longest <= 9, qPrintable(QString::number(longest))); } int main(int argc, char **argv) { // Every widget on this screen scales by two. qputenv("QT_SCALE_FACTOR", "2"); QApplication app(argc, argv); HighDpiCanvasTests tests; return QTest::qExec(&tests, argc, argv); } #include "HighDpiCanvasTests.moc"