codec_comparison_window.cc (11211B)
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/comparison_viewer/codec_comparison_window.h" 7 8 #include <stdlib.h> 9 10 #include <QCollator> 11 #include <QComboBox> 12 #include <QDir> 13 #include <QFileInfo> 14 #include <QFlags> 15 #include <QIcon> 16 #include <QImage> 17 #include <QImageReader> 18 #include <QLabel> 19 #include <QList> 20 #include <QMap> 21 #include <QString> 22 #include <QStringList> 23 #include <QtConcurrent> 24 #include <algorithm> 25 #include <climits> 26 #include <functional> 27 #include <utility> 28 29 #include "lib/extras/codec.h" 30 #include "tools/comparison_viewer/image_loading.h" 31 #include "tools/comparison_viewer/split_image_view.h" 32 #include "tools/icc_detect/icc_detect.h" 33 34 namespace jpegxl { 35 namespace tools { 36 37 static constexpr char kPngSuffix[] = "png"; 38 39 namespace { 40 41 QVector<QPair<QComboBox*, QString>> currentCodecSelection( 42 const Ui::CodecComparisonWindow& ui) { 43 QVector<QPair<QComboBox*, QString>> result; 44 for (QComboBox* const comboBox : 45 {ui.codec1ComboBox, ui.codec2ComboBox, ui.compressionLevel1ComboBox, 46 ui.compressionLevel2ComboBox}) { 47 result << qMakePair(comboBox, comboBox->currentText()); 48 } 49 return result; 50 } 51 52 void restoreCodecSelection( 53 const QVector<QPair<QComboBox*, QString>>& selection) { 54 for (const auto& comboBox : selection) { 55 const int index = comboBox.first->findText(comboBox.second); 56 if (index != -1) { 57 comboBox.first->setCurrentIndex(index); 58 } 59 } 60 } 61 62 } // namespace 63 64 CodecComparisonWindow::CodecComparisonWindow(const QString& directory, 65 const float intensityTarget, 66 QWidget* const parent) 67 : QMainWindow(parent), 68 intensityTarget_(intensityTarget), 69 monitorIccProfile_(GetMonitorIccProfile(this)) { 70 ui_.setupUi(this); 71 72 connect(ui_.imageSetComboBox, &QComboBox::currentTextChanged, this, 73 &CodecComparisonWindow::handleImageSetSelection); 74 connect(ui_.imageComboBox, &QComboBox::currentTextChanged, this, 75 &CodecComparisonWindow::handleImageSelection); 76 77 connect(ui_.codec1ComboBox, &QComboBox::currentTextChanged, 78 [this]() { handleCodecChange(Side::LEFT); }); 79 connect(ui_.codec2ComboBox, &QComboBox::currentTextChanged, 80 [this]() { handleCodecChange(Side::RIGHT); }); 81 82 connect(ui_.compressionLevel1ComboBox, &QComboBox::currentTextChanged, 83 [this]() { updateSideImage(Side::LEFT); }); 84 connect(ui_.compressionLevel2ComboBox, &QComboBox::currentTextChanged, 85 [this]() { updateSideImage(Side::RIGHT); }); 86 87 connect(ui_.match1Label, &QLabel::linkActivated, 88 [this]() { matchSize(Side::LEFT); }); 89 connect(ui_.match2Label, &QLabel::linkActivated, 90 [this]() { matchSize(Side::RIGHT); }); 91 92 connect( 93 ui_.splitImageView, &SplitImageView::renderingModeChanged, 94 [this](const SplitImageRenderer::RenderingMode newMode) { 95 switch (newMode) { 96 case SplitImageRenderer::RenderingMode::LEFT: 97 case SplitImageRenderer::RenderingMode::RIGHT: { 98 QString codec, compressionLevel; 99 if (newMode == SplitImageRenderer::RenderingMode::LEFT) { 100 codec = ui_.codec1ComboBox->currentText(); 101 compressionLevel = ui_.compressionLevel1ComboBox->currentText(); 102 } else { 103 codec = ui_.codec2ComboBox->currentText(); 104 compressionLevel = ui_.compressionLevel2ComboBox->currentText(); 105 } 106 ui_.renderingModeLabel->setText(tr("Currently displaying: %1 @ %2") 107 .arg(codec) 108 .arg(compressionLevel)); 109 break; 110 } 111 112 case SplitImageRenderer::RenderingMode::MIDDLE: 113 ui_.renderingModeLabel->setText( 114 tr("Currently displaying the original image.")); 115 break; 116 117 default: 118 ui_.renderingModeLabel->clear(); 119 break; 120 } 121 }); 122 123 loadDirectory(directory); 124 } 125 126 void CodecComparisonWindow::handleImageSetSelection( 127 const QString& imageSetName) { 128 const auto selection = currentCodecSelection(ui_); 129 { 130 const QSignalBlocker blocker(ui_.imageComboBox); 131 ui_.imageComboBox->clear(); 132 } 133 const QStringList imageNames = imageSets_.value(imageSetName).keys(); 134 const std::function<QIcon(const QString&)> loadIcon = 135 [this, &imageSetName](const QString& imageName) { 136 return QIcon(pathToOriginalImage(imageSetName, imageName)); 137 }; 138 const QFuture<QIcon> thumbnails = QtConcurrent::mapped(imageNames, loadIcon); 139 int i = 0; 140 for (const QString& imageName : imageNames) { 141 ui_.imageComboBox->addItem(thumbnails.resultAt(i), imageName); 142 ++i; 143 } 144 restoreCodecSelection(selection); 145 } 146 147 void CodecComparisonWindow::handleImageSelection(const QString& imageName) { 148 const QString imageSetName = ui_.imageSetComboBox->currentText(); 149 ui_.splitImageView->setMiddleImage( 150 loadImage(pathToOriginalImage(imageSetName, imageName), 151 monitorIccProfile_, intensityTarget_)); 152 153 const auto selection = currentCodecSelection(ui_); 154 QStringList codecs = imageSets_.value(imageSetName).value(imageName).keys(); 155 for (QComboBox* const codecComboBox : 156 {ui_.codec1ComboBox, ui_.codec2ComboBox}) { 157 { 158 const QSignalBlocker blocker(codecComboBox); 159 codecComboBox->clear(); 160 } 161 codecComboBox->addItems(codecs); 162 } 163 restoreCodecSelection(selection); 164 } 165 166 void CodecComparisonWindow::handleCodecChange(const Side side) { 167 const QComboBox* const codecComboBox = 168 side == Side::LEFT ? ui_.codec1ComboBox : ui_.codec2ComboBox; 169 QComboBox* const compressionLevelComboBox = 170 side == Side::LEFT ? ui_.compressionLevel1ComboBox 171 : ui_.compressionLevel2ComboBox; 172 173 QStringList compressionLevels = 174 imageSets_.value(ui_.imageSetComboBox->currentText()) 175 .value(ui_.imageComboBox->currentText()) 176 .value(codecComboBox->currentText()) 177 .keys(); 178 QCollator collator; 179 collator.setNumericMode(true); 180 std::sort(compressionLevels.begin(), compressionLevels.end(), collator); 181 182 { 183 const QSignalBlocker blocker(compressionLevelComboBox); 184 compressionLevelComboBox->clear(); 185 } 186 compressionLevelComboBox->addItems(compressionLevels); 187 matchSize(side); 188 } 189 190 void CodecComparisonWindow::updateSideImage(const Side side) { 191 const ComparableImage& imageInfo = currentlySelectedImage(side); 192 if (imageInfo.decodedImagePath.isEmpty()) return; 193 QImage image = loadImage(imageInfo.decodedImagePath, monitorIccProfile_, 194 intensityTarget_); 195 const int pixels = image.width() * image.height(); 196 QLabel* const sizeInfoLabel = 197 side == Side::LEFT ? ui_.size1Label : ui_.size2Label; 198 if (pixels == 0) { 199 sizeInfoLabel->setText(tr("Empty image.")); 200 } else { 201 const double bpp = 202 CHAR_BIT * static_cast<double>(imageInfo.byteSize) / pixels; 203 sizeInfoLabel->setText(tr("%L1bpp").arg(bpp)); 204 } 205 206 if (side == Side::LEFT) { 207 ui_.splitImageView->setLeftImage(std::move(image)); 208 } else { 209 ui_.splitImageView->setRightImage(std::move(image)); 210 } 211 } 212 213 QString CodecComparisonWindow::pathToOriginalImage( 214 const QString& imageSetName, const QString& imageName) const { 215 return baseDirectory_.absolutePath() + "/" + imageSetName + "/" + imageName + 216 "/original.png"; 217 } 218 219 CodecComparisonWindow::ComparableImage 220 CodecComparisonWindow::currentlySelectedImage(const Side side) const { 221 const QComboBox* const codecComboBox = 222 side == Side::LEFT ? ui_.codec1ComboBox : ui_.codec2ComboBox; 223 QComboBox* const compressionLevelComboBox = 224 side == Side::LEFT ? ui_.compressionLevel1ComboBox 225 : ui_.compressionLevel2ComboBox; 226 227 return imageSets_.value(ui_.imageSetComboBox->currentText()) 228 .value(ui_.imageComboBox->currentText()) 229 .value(codecComboBox->currentText()) 230 .value(compressionLevelComboBox->currentText()); 231 } 232 233 void CodecComparisonWindow::matchSize(const Side side) { 234 const Side otherSide = (side == Side::LEFT ? Side::RIGHT : Side::LEFT); 235 const qint64 otherSideSize = currentlySelectedImage(otherSide).byteSize; 236 if (otherSideSize == 0) return; 237 238 const QComboBox* const codecComboBox = 239 side == Side::LEFT ? ui_.codec1ComboBox : ui_.codec2ComboBox; 240 QComboBox* const compressionLevelComboBox = 241 side == Side::LEFT ? ui_.compressionLevel1ComboBox 242 : ui_.compressionLevel2ComboBox; 243 const Codec codec = imageSets_.value(ui_.imageSetComboBox->currentText()) 244 .value(ui_.imageComboBox->currentText()) 245 .value(codecComboBox->currentText()); 246 if (codec.empty()) return; 247 Codec::ConstIterator bestMatch = codec.begin(); 248 for (auto it = codec.begin(); it != codec.end(); ++it) { 249 if (std::abs(it->byteSize - otherSideSize) < 250 std::abs(bestMatch->byteSize - otherSideSize)) { 251 bestMatch = it; 252 } 253 } 254 compressionLevelComboBox->setCurrentText(bestMatch.key()); 255 } 256 257 void CodecComparisonWindow::loadDirectory(const QString& directory) { 258 baseDirectory_.setPath(directory); 259 baseDirectory_.makeAbsolute(); 260 imageSets_.clear(); 261 visited_.clear(); 262 263 browseDirectory(directory); 264 265 { 266 const QSignalBlocker blocker(ui_.imageSetComboBox); 267 ui_.imageSetComboBox->clear(); 268 } 269 ui_.imageSetComboBox->addItems(imageSets_.keys()); 270 } 271 272 void CodecComparisonWindow::browseDirectory(const QDir& directory, int depth) { 273 for (const QFileInfo& subdirectory : directory.entryInfoList( 274 QDir::Dirs | QDir::NoDotAndDotDot | QDir::NoSymLinks)) { 275 if (visited_.contains(subdirectory.absoluteFilePath())) continue; 276 visited_.insert(subdirectory.absoluteFilePath()); 277 browseDirectory(subdirectory.absoluteFilePath(), depth + 1); 278 } 279 280 // Need at least image_name/codec_name/file. 281 if (depth < 2) return; 282 283 for (const QFileInfo& file : directory.entryInfoList(QDir::Files)) { 284 if (file.suffix() == kPngSuffix) continue; 285 QString decodedImage; 286 if (canLoadImageWithExtension(file.suffix())) { 287 decodedImage = file.absoluteFilePath(); 288 } else { 289 QFileInfo png(file.absolutePath() + "/" + file.completeBaseName() + "." + 290 kPngSuffix); 291 if (png.exists()) { 292 decodedImage = png.absoluteFilePath(); 293 } 294 } 295 296 if (decodedImage.isEmpty()) continue; 297 298 const QString codec = file.absoluteDir().dirName(); 299 QDir imageDirectory = file.absoluteDir(); 300 if (!imageDirectory.cdUp()) return; 301 const QString imageName = imageDirectory.dirName(); 302 QDir imageSetDirectory = imageDirectory; 303 if (!imageSetDirectory.cdUp()) return; 304 QString imageSetPath = 305 baseDirectory_.relativeFilePath(imageSetDirectory.absolutePath()); 306 if (imageSetPath.isEmpty()) { 307 imageSetPath = "."; 308 } 309 310 ComparableImage& image = 311 imageSets_[imageSetPath][imageName][codec][file.completeBaseName()]; 312 image.decodedImagePath = decodedImage; 313 image.byteSize = file.size(); 314 } 315 } 316 317 } // namespace tools 318 } // namespace jpegxl