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