1 // Copyright (c) the JPEG XL Project Authors. All rights reserved.
2 //
3 // Use of this source code is governed by a BSD-style
4 // license that can be found in the LICENSE file.
5 
6 #include "tools/flicker_test/test_window.h"
7 
8 #include <QDir>
9 #include <QMessageBox>
10 #include <QSet>
11 #include <algorithm>
12 #include <random>
13 
14 #include "tools/icc_detect/icc_detect.h"
15 
16 namespace jxl {
17 
FlickerTestWindow(FlickerTestParameters parameters,QWidget * const parent)18 FlickerTestWindow::FlickerTestWindow(FlickerTestParameters parameters,
19                                      QWidget* const parent)
20     : QMainWindow(parent),
21       monitorProfile_(GetMonitorIccProfile(this)),
22       parameters_(std::move(parameters)),
23       originalFolder_(parameters_.originalFolder, "*.png"),
24       alteredFolder_(parameters_.alteredFolder, "*.png"),
25       outputFile_(parameters_.outputFile) {
26   ui_.setupUi(this);
27   ui_.splitView->setSpacing(parameters_.spacing);
28   ui_.endLabel->setText(
29       tr("The test is complete and the results have been saved to \"%1\".")
30           .arg(parameters_.outputFile));
31   connect(ui_.startButton, &QAbstractButton::clicked, [&] {
32     ui_.stackedView->setCurrentWidget(ui_.splitView);
33     nextImage();
34   });
35   connect(ui_.splitView, &SplitView::testResult, this,
36           &FlickerTestWindow::processTestResult);
37 
38   if (!outputFile_.open(QIODevice::WriteOnly)) {
39     QMessageBox messageBox;
40     messageBox.setIcon(QMessageBox::Critical);
41     messageBox.setStandardButtons(QMessageBox::Close);
42     messageBox.setWindowTitle(tr("Failed to open output file"));
43     messageBox.setInformativeText(
44         tr("Could not open \"%1\" for writing.").arg(outputFile_.fileName()));
45     messageBox.exec();
46     proceed_ = false;
47     return;
48   }
49   outputStream_.setDevice(&outputFile_);
50   outputStream_ << "image name,original side,clicked side,click delay (ms)\n";
51 
52   if (monitorProfile_.isEmpty()) {
53     QMessageBox messageBox;
54     messageBox.setIcon(QMessageBox::Warning);
55     messageBox.setStandardButtons(QMessageBox::Ok);
56     messageBox.setWindowTitle(tr("No monitor profile found"));
57     messageBox.setText(
58         tr("No ICC profile appears to be associated with the display. It will "
59            "be assumed to match sRGB."));
60     messageBox.exec();
61   }
62 
63   originalFolder_.setFilter(QDir::Files);
64   alteredFolder_.setFilter(QDir::Files);
65 
66 #if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
67   auto originalImages = QSet<QString>::fromList(originalFolder_.entryList());
68   auto alteredImages = QSet<QString>::fromList(alteredFolder_.entryList());
69 #else
70   const QStringList originalFolderEntries = originalFolder_.entryList();
71   QSet<QString> originalImages(originalFolderEntries.begin(),
72                                originalFolderEntries.end());
73   const QStringList alteredFolderEntries = alteredFolder_.entryList();
74   QSet<QString> alteredImages(alteredFolderEntries.begin(),
75                               alteredFolderEntries.end());
76 #endif
77 
78   auto onlyOriginal = originalImages - alteredImages,
79        onlyAltered = alteredImages - originalImages;
80   if (!onlyOriginal.isEmpty() || !onlyAltered.isEmpty()) {
81     QMessageBox messageBox;
82     messageBox.setIcon(QMessageBox::Warning);
83     messageBox.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel);
84     messageBox.setWindowTitle(tr("Image set mismatch"));
85     messageBox.setText(
86         tr("A mismatch has been detected between the original and altered "
87            "images."));
88     messageBox.setInformativeText(tr("Proceed with the test?"));
89     QStringList detailedTextParagraphs;
90     const QString itemFormat = tr("— %1\n");
91     if (!onlyOriginal.isEmpty()) {
92       QString originalList;
93       for (const QString& original : onlyOriginal) {
94         originalList += itemFormat.arg(original);
95       }
96       detailedTextParagraphs << tr("The following images were only found in "
97                                    "the originals folder:\n%1")
98                                     .arg(originalList);
99     }
100     if (!onlyAltered.isEmpty()) {
101       QString alteredList;
102       for (const QString& altered : onlyAltered) {
103         alteredList += itemFormat.arg(altered);
104       }
105       detailedTextParagraphs << tr("The following images were only found in "
106                                    "the altered images folder:\n%1")
107                                     .arg(alteredList);
108     }
109     messageBox.setDetailedText(detailedTextParagraphs.join("\n\n"));
110     if (messageBox.exec() == QMessageBox::Cancel) {
111       proceed_ = false;
112       return;
113     }
114   }
115 
116   remainingImages_ = originalImages.intersect(alteredImages).values();
117   std::random_device rd;
118   std::mt19937 g(rd());
119   std::shuffle(remainingImages_.begin(), remainingImages_.end(), g);
120 }
121 
processTestResult(const QString & imageName,const SplitView::Side originalSide,const SplitView::Side clickedSide,const int clickDelayMSecs)122 void FlickerTestWindow::processTestResult(const QString& imageName,
123                                           const SplitView::Side originalSide,
124                                           const SplitView::Side clickedSide,
125                                           const int clickDelayMSecs) {
126   const auto sideToString = [](const SplitView::Side side) {
127     switch (side) {
128       case SplitView::Side::kLeft:
129         return "left";
130 
131       case SplitView::Side::kRight:
132         return "right";
133     }
134     return "unknown";
135   };
136   outputStream_ << imageName << "," << sideToString(originalSide) << ","
137                 << sideToString(clickedSide) << "," << clickDelayMSecs << "\n";
138 
139   nextImage();
140 }
141 
nextImage()142 void FlickerTestWindow::nextImage() {
143   if (remainingImages_.empty()) {
144     outputStream_.flush();
145     ui_.stackedView->setCurrentWidget(ui_.finalPage);
146     return;
147   }
148   const QString image = remainingImages_.takeFirst();
149 retry:
150   QImage originalImage =
151       loadImage(originalFolder_.absoluteFilePath(image), monitorProfile_);
152   QImage alteredImage =
153       loadImage(alteredFolder_.absoluteFilePath(image), monitorProfile_);
154   if (originalImage.isNull() || alteredImage.isNull()) {
155     QMessageBox messageBox(this);
156     messageBox.setIcon(QMessageBox::Warning);
157     messageBox.setStandardButtons(QMessageBox::Retry | QMessageBox::Ignore |
158                                   QMessageBox::Abort);
159     messageBox.setWindowTitle(tr("Failed to load image"));
160     messageBox.setText(tr("Could not load image \"%1\".").arg(image));
161     switch (messageBox.exec()) {
162       case QMessageBox::Retry:
163         goto retry;
164 
165       case QMessageBox::Ignore:
166         outputStream_ << image << ",,,\n";
167         return nextImage();
168 
169       case QMessageBox::Abort:
170         ui_.stackedView->setCurrentWidget(ui_.finalPage);
171         return;
172     }
173   }
174 
175   ui_.splitView->setOriginalImage(std::move(originalImage));
176   ui_.splitView->setAlteredImage(std::move(alteredImage));
177   ui_.splitView->startTest(
178       image, parameters_.blankingTimeMSecs, parameters_.viewingTimeSecs,
179       parameters_.advanceTimeMSecs, parameters_.gray,
180       parameters_.grayFadingTimeMSecs, parameters_.grayTimeMSecs);
181 }
182 
183 }  // namespace jxl
184