#include "Document/BrushStroke.h" #include "Document/EditorSession.h" #include "UI/CameraRawColorControls.h" #include "UI/CameraRawControls.h" #include "UI/CameraRawDetailOpticsControls.h" #include "UI/CameraRawGeometryCalibrationControls.h" #include "UI/FloatingPanel.h" #include "UI/LayerIcons.h" #include #include #include #include #include #include #include #include #include #include // Swift's Color Grading, Detail, Optics, Geometry and Calibration groups. namespace { // The knob's middle, where a double click resets. QPoint knob(const CameraRawSlider &slider) { QStyleOptionSlider option; option.initFrom(&slider); option.orientation = Qt::Horizontal; option.minimum = slider.minimum(); option.maximum = slider.maximum(); option.sliderPosition = slider.sliderPosition(); option.sliderValue = slider.value(); return slider.style()->subControlRect(QStyle::CC_Slider, &option, QStyle::SC_SliderHandle, &slider).center(); } template struct Group { EditorSession session; std::unique_ptr controls; Group() { session.createDocument(8, 8); QImage image = BrushRaster::context(8, 8, false); image.fill(QColor(200, 120, 60)); session.insert(ImportedImage(image, image, QStringLiteral("Warm"))); session.beginFilter(FilterKind::cameraRaw); controls = std::make_unique(session); controls->resize(374, 700); controls->show(); } template Widget &child(const QString &name) const { auto *found = controls->template findChild(name); if (!found) throw std::runtime_error(name.toStdString()); return *found; } const CameraRawSettings &raw() const { return session.filterEdit().value().settings.cameraRaw; } const CameraRawPanel &panel() const { return session.filterEdit().value().rawPanel; } void set(const std::function &change) { FilterSettings settings = session.filterEdit().value().settings; change(settings.cameraRaw); session.updateFilter(settings, true); } }; } class CameraRawGradingLensTests : public QObject { Q_OBJECT private slots: void theWheelsSetHueAndSaturationAndResetOnADoubleClick(); void theWheelsHueRunsCounterclockwise(); void onePageShowsOneWheelAndTheGradingFitsTheDock(); void detailRoundsItsRowsAndDimsIdleNoiseDetail(); void opticsTogglesProfileDefringeAndHueRanges(); void geometryUprightGuidesAndRoundedRows(); void calibrationChoosesItsProcessAndPrimaries(); }; void CameraRawGradingLensTests::theWheelsSetHueAndSaturationAndResetOnADoubleClick() { Group group; auto *box = group.controls->findChild(QStringLiteral("gradeWheelMidtones")); QWidget *wheel = nullptr; for (QWidget *child : box->findChildren()) if (child->size() == QSize(86, 86)) wheel = child; QVERIFY(wheel); // Straight up is hue 90; at the rim, 100. QTest::mousePress(wheel, Qt::LeftButton, {}, QPoint(43, 6)); QTest::mouseRelease(wheel, Qt::LeftButton, {}, QPoint(43, 6)); QVERIFY(std::abs(group.raw().grading.midtones.hue - 90) < 0.5 && group.raw().grading.midtones.saturation == 100); QVERIFY(group.raw().grading.shadows.hue == 0); QCOMPARE(box->findChild(QStringLiteral("gradeReadout"))->text(), QString("90° 100")); // The left rim, 37 points out: hue 180, saturation 100. QTest::mousePress(wheel, Qt::LeftButton, {}, QPoint(43 - 37, 43)); QVERIFY(std::abs(group.raw().grading.midtones.hue - 180) < 0.5 && group.raw().grading.midtones.saturation == 100); QTest::mouseRelease(wheel, Qt::LeftButton, {}, QPoint(6, 43)); // A press outside the circle does nothing. QTest::mousePress(wheel, Qt::LeftButton, {}, QPoint(1, 1)); QVERIFY(std::abs(group.raw().grading.midtones.hue - 180) < 0.5); QTest::mouseRelease(wheel, Qt::LeftButton, {}, QPoint(1, 1)); QTest::mouseDClick(wheel, Qt::LeftButton, {}, QPoint(43, 43)); QVERIFY(group.raw().grading.midtones.hue == 0 && group.raw().grading.midtones.saturation == 0); auto *luminance = box->findChild(QStringLiteral("gradeLuminance")); luminance->setValue(750); QCOMPARE(group.raw().grading.midtones.luminance, 50.0); QCOMPARE(luminance->width(), 96); group.child(QStringLiteral("gradeBlendingSlider")).setValue(200); QCOMPARE(group.raw().grading.blending, 20.0); auto &balance = group.child(QStringLiteral("gradeBalanceSlider")); balance.setValue(250); QCOMPARE(group.raw().grading.balance, -50.0); QTest::mouseDClick(&balance, Qt::LeftButton, {}, knob(balance)); QCOMPARE(group.raw().grading.balance, 0.0); } // 1.2.9's 96edd5f: the rainbow runs the way the hue does. void CameraRawGradingLensTests::theWheelsHueRunsCounterclockwise() { Group group; auto *box = group.controls->findChild(QStringLiteral("gradeWheelShadows")); QWidget *wheel = nullptr; for (QWidget *child : box->findChildren()) if (child->size() == QSize(86, 86)) wheel = child; const QImage drawn = wheel->grab().toImage(); const auto hueAt = [&drawn](double degrees) { const double radians = degrees * std::numbers::pi / 180; return drawn.pixelColor(int(43 + std::cos(radians) * 30), int(43 - std::sin(radians) * 30)).hsvHueF() * 360; }; // Dragged to 120, the wheel shows green; 240, blue. QVERIFY2(std::abs(hueAt(120) - 120) < 20, qPrintable(QString::number(hueAt(120)))); QVERIFY2(std::abs(hueAt(240) - 240) < 20, qPrintable(QString::number(hueAt(240)))); QVERIFY2(std::abs(hueAt(60) - 60) < 20, qPrintable(QString::number(hueAt(60)))); } void CameraRawGradingLensTests::onePageShowsOneWheelAndTheGradingFitsTheDock() { Group group; auto &page = group.child(QStringLiteral("gradePage")); QCOMPARE(page.count(), 5); const auto shown = [&group] { QStringList names; for (QWidget *box : group.controls->findChildren()) if (box->objectName().startsWith(QLatin1String("gradeWheel")) && box->isVisible()) names << box->objectName(); names.sort(); return names; }; QCOMPARE(shown(), (QStringList{"gradeWheelHighlights", "gradeWheelMidtones", "gradeWheelShadows"})); emit page.activated(4); QVERIFY(group.panel().gradePage == CameraRawGradePage::global); QCOMPARE(shown(), QStringList{"gradeWheelGlobal"}); emit page.activated(2); QCOMPARE(shown(), QStringList{"gradeWheelMidtones"}); // Swift's theDockedPanelFitsTheGradingWheels: padding 24 a side, inset 18. CameraRawGradingControls grading(group.session); QVERIFY2(grading.sizeHint().width() <= FloatingPanel::dockedWidth - 48 - 18, qPrintable(QString::number(grading.sizeHint().width()))); } void CameraRawGradingLensTests::detailRoundsItsRowsAndDimsIdleNoiseDetail() { Group group; auto &amount = group.child(QStringLiteral("sharpenAmountSlider")); amount.setValue(333); QCOMPARE(group.raw().detail.sharpenAmount, 50.0); auto &radius = group.child(QStringLiteral("sharpenRadiusSlider")); QTest::mouseDClick(&radius, Qt::LeftButton, {}, knob(radius)); QCOMPARE(group.raw().detail.sharpenRadius, 10.0); auto &detail = group.child(QStringLiteral("noiseLuminanceDetailSlider")); QVERIFY(!detail.isEnabled()); group.child(QStringLiteral("noiseLuminanceSlider")).setValue(400); QVERIFY(detail.isEnabled() && !group.child(QStringLiteral("noiseColorDetailSlider")).isEnabled()); // Alt on Masking shows the mask; other rows clear it. QWindow *window = group.controls->windowHandle(); QTest::keyPress(window, Qt::Key_Alt, Qt::AltModifier); group.child(QStringLiteral("sharpenMaskingSlider")).setValue(500); QVERIFY(group.panel().sharpenMask && group.raw().detail.sharpenMasking == 50); amount.setValue(400); QVERIFY(!group.panel().sharpenMask); QTest::keyRelease(window, Qt::Key_Alt); group.child(QStringLiteral("sharpenMaskingSlider")).setValue(600); QVERIFY(!group.panel().sharpenMask); } void CameraRawGradingLensTests::opticsTogglesProfileDefringeAndHueRanges() { Group group; auto &profile = group.child(QStringLiteral("profileDistortionSlider")); QVERIFY(!profile.isVisible()); group.child(QStringLiteral("enableLensProfile")).click(); QVERIFY(group.raw().optics.enableLensProfile && profile.isVisible()); group.child(QStringLiteral("removeChromaticAberration")).click(); QVERIFY(group.raw().optics.removeChromaticAberration); // A range from zero rounds; a signed range does not. group.child(QStringLiteral("purpleAmountSlider")).setValue(333); QCOMPARE(group.raw().optics.purpleAmount, 33.0); group.child(QStringLiteral("distortionSlider")).setValue(333); QVERIFY(std::abs(group.raw().optics.distortion - -33.4) < 1e-9); auto &midpoint = group.child(QStringLiteral("lensMidpointSlider")); midpoint.setValue(0); QTest::mouseDClick(&midpoint, Qt::LeftButton, {}, knob(midpoint)); QCOMPARE(group.raw().optics.vignetteMidpoint, 50.0); // Hue sliders round, and reset to their own ends. auto &low = group.child(QStringLiteral("greenHueLow")); low.setValue(111); QCOMPARE(group.raw().optics.greenHueLow, 40.0); QTest::mouseDClick(&low, Qt::LeftButton, {}, knob(low)); QCOMPARE(group.raw().optics.greenHueLow, 60.0); auto &high = group.child(QStringLiteral("purpleHueHigh")); QTest::mouseDClick(&high, Qt::LeftButton, {}, knob(high)); QCOMPARE(group.raw().optics.purpleHueHigh, 310.0); auto &hint = group.child(QStringLiteral("defringeHint")); QVERIFY(!hint.isVisible()); group.child(QStringLiteral("defringeSampler")).click(); QVERIFY(group.panel().samplesDefringe && hint.isVisible()); } void CameraRawGradingLensTests::geometryUprightGuidesAndRoundedRows() { Group group; auto &draw = group.child(QStringLiteral("drawGuides")); QVERIFY(!draw.isVisible()); group.child(QStringLiteral("upright1")).click(); QVERIFY(group.raw().geometry.upright == CameraRawUprightMode::guided && draw.isVisible()); QVERIFY(!group.child(QStringLiteral("clearGuides")).isVisible()); const QImage line = LayerIcons::pixmap(LayerIcon::lineDiagonal, 14, draw.palette().color(QPalette::ButtonText), draw.devicePixelRatioF()).toImage(); QVERIFY(draw.icon().pixmap(QSize(14, 14), draw.devicePixelRatioF()).toImage() == line); draw.click(); QVERIFY(group.panel().drawingGeometryGuide && group.child(QStringLiteral("drawGuidesHint")).isVisible()); group.set([](CameraRawSettings &settings) { settings.geometry.guides = {{0.1, 0.5, 0.9, 0.5}}; }); group.child(QStringLiteral("clearGuides")).click(); QVERIFY(group.raw().geometry.guides.empty()); // Off puts the guide tool away. group.child(QStringLiteral("upright0")).click(); QVERIFY(group.raw().geometry.upright == CameraRawUprightMode::off && !group.panel().drawingGeometryGuide); emit group.child(QStringLiteral("projection")).activated(1); QVERIFY(group.raw().geometry.projection == CameraRawProjection::rectilinear); group.child(QStringLiteral("rotateSlider")).setValue(333); QCOMPARE(group.raw().geometry.rotate, -15.0); group.child(QStringLiteral("offsetYSlider")).setValue(333); QCOMPARE(group.raw().geometry.offsetY, -33.0); group.child(QStringLiteral("constrainCrop")).click(); QVERIFY(group.raw().geometry.constrainCrop); } void CameraRawGradingLensTests::calibrationChoosesItsProcessAndPrimaries() { Group group; auto &process = group.child(QStringLiteral("processVersion")); QCOMPARE(process.currentText(), QString("Version 6")); QCOMPARE(group.child(QStringLiteral("processSummary")).text(), summary(CameraRawProcessVersion::version6)); emit process.activated(1); QVERIFY(group.raw().calibration.process == CameraRawProcessVersion::version2); QCOMPARE(group.child(QStringLiteral("processSummary")).text(), summary(CameraRawProcessVersion::version2)); for (const auto &[name, member] : {std::pair("shadowTint", &CameraRawCalibrationSettings::shadowTint), std::pair("redHue", &CameraRawCalibrationSettings::redHue), std::pair("redSaturation", &CameraRawCalibrationSettings::redSaturation), std::pair("greenHue", &CameraRawCalibrationSettings::greenHue), std::pair("greenSaturation", &CameraRawCalibrationSettings::greenSaturation), std::pair("blueHue", &CameraRawCalibrationSettings::blueHue), std::pair("blueSaturation", &CameraRawCalibrationSettings::blueSaturation)}) { auto &slider = group.child(QString::fromLatin1(name) + QStringLiteral("Slider")); slider.setValue(666); QVERIFY2(group.raw().calibration.*member == 33, name); QTest::mouseDClick(&slider, Qt::LeftButton, {}, knob(slider)); QVERIFY2(group.raw().calibration.*member == 0, name); } } QTEST_MAIN(CameraRawGradingLensTests) #include "CameraRawGradingLensTests.moc"