#include "CameraRawFixtures.h" #include "Document/Filters.h" #include #include // Swift's CameraRawTests: the grade on pixels, group by group. using namespace CameraRawFixtures; class CameraRawTests : public QObject { Q_OBJECT private slots: void defaultsLeavePixelsAndAlphaAlone(); void exposureAndContrastMatchPhotoshop(); void tonalSlidersMoveTheEndTheyName(); void temperatureWarmsAndTintMovesTowardMagenta(); void whiteBalanceAndVibranceMatchPhotoshop(); void mixerCalibrationCurveAndVignettesMatchPhotoshop(); void vibranceFavorsDullColorsAndProtectsSkinWhileSaturationDoesNot(); void textureAndClaritySharpenAnEdgeAndLeaveAFlatField(); void dehazeDeepensOrLiftsAndKeepsAlpha(); void glowIsIdleAtZeroAndHalationFringeIsRedderThanDiffusion(); void vignetteDarkensCornersAndHighlightsOnlyWhileDarkening(); void grainIsStableAndTheEffectsEyeDropsTheWholeGroup(); void histogramFollowsTheGradeAndClippingPaintStaysOffTheResult(); void curveMixerAndGradingChangeOnlyTheirOwnTones(); void detailSharpeningNoiseAndMaskingPreview(); void opticsDistortionDefringeAndDetailEye(); }; void CameraRawTests::defaultsLeavePixelsAndAlphaAlone() { const QImage input = gray(4, 4, 0.5); QVERIFY(pixels(CameraRawSettings().apply(input)) == pixels(input)); CameraRawSettings broken; broken.exposure = std::nan(""); broken.temperature = 400; QVERIFY(broken.normalized().exposure == 0 && broken.normalized().temperature == 100); QVERIFY(!isImageAdjustment(FilterKind::cameraRaw)); } // Photoshop's Camera Raw Filter's own numbers, measured. void CameraRawTests::exposureAndContrastMatchPhotoshop() { const QImage input = gray(); CameraRawSettings settings; settings.exposure = 1; const std::array brighter = pixels(settings.apply(input))[0]; QVERIFY2(std::abs(brighter[0] - 181) <= 2, qPrintable(QString::number(brighter[0]))); QVERIFY(brighter[0] == brighter[1] && brighter[1] == brighter[2]); const QImage translucent = gray(4, 4, 0.5); QVERIFY((pixels(settings.apply(translucent))[0] == std::array{181, 181, 181, pixels(translucent)[0][3]})); settings = CameraRawSettings(); settings.contrast = 100; const auto pushed = pixels(settings.apply(pair(64, 192))); QVERIFY2(std::abs(pushed[0][0] - 36) <= 3 && std::abs(pushed[1][0] - 213) <= 3, "contrast +100 drives the pair apart"); settings.contrast = -100; const auto flat = pixels(settings.apply(pair(64, 192))); QVERIFY2(std::abs(flat[0][0] - 88) <= 3 && std::abs(flat[1][0] - 174) <= 3, "contrast −100 draws them together"); } void CameraRawTests::tonalSlidersMoveTheEndTheyName() { const QImage brightAndMid = pair(230, 128); CameraRawSettings settings; settings.highlights = -100; const auto recovered = pixels(settings.apply(brightAndMid)); // Photoshop: 230 to 209, mid gray kept. QVERIFY(recovered[0][0] < 222 && std::abs(recovered[1][0] - 128) <= 4); // Highlights alone reads the picture's log brightness. const auto bright = pixels(settings.apply(pair(220, 250))); QVERIFY((bright[0] == std::array{197, 197, 197, 255}) && (bright[1] == std::array{239, 240, 240, 255})); // Highlights before Whites, as Camera Raw orders them. settings.whites = 100; const auto both = pixels(settings.apply(pair(128, 230))); QVERIFY(both[0][0] == 179 && both[1][0] == 255); settings = CameraRawSettings(); settings.whites = 100; const auto clipped = pixels(settings.apply(brightAndMid)); QVERIFY2(clipped[0][0] == 255 && clipped[1][0] > 128, "whites lift the midtone too, as in Photoshop"); const auto view = pixels(settings.apply(brightAndMid, CameraRawClipping::highlights)); QVERIFY2((view[0] == std::array{255, 255, 255, 255}) && (view[1] == std::array{0, 0, 0, 255}), "highlight clipping is not the grade"); const QImage darkAndMid = pair(20, 128); settings = CameraRawSettings(); settings.shadows = 100; const auto opened = pixels(settings.apply(darkAndMid)); // Photoshop: 20 to 33, mid gray kept. QVERIFY(opened[0][0] > 22 && std::abs(opened[1][0] - 128) <= 6); settings = CameraRawSettings(); settings.blacks = -100; const auto crushed = pixels(settings.apply(darkAndMid)); QVERIFY2(crushed[0][0] == 0 && std::abs(crushed[1][0] - 91) <= 3, "blacks darken the midtone too"); const auto shadowView = pixels(settings.apply(darkAndMid, CameraRawClipping::shadows)); QVERIFY2((shadowView[0] == std::array{0, 0, 0, 255}) && (shadowView[1] == std::array{255, 255, 255, 255}), "shadow clipping is not the grade"); } void CameraRawTests::temperatureWarmsAndTintMovesTowardMagenta() { const QImage input = gray(); CameraRawSettings settings; settings.temperature = 100; const std::array warm = pixels(settings.apply(input))[0]; QVERIFY(warm[0] > 128 && warm[2] < 128 && warm[0] > warm[2]); settings = CameraRawSettings(); settings.tint = 100; const std::array magenta = pixels(settings.apply(input))[0]; QVERIFY(magenta[1] < 128 && magenta[1] < magenta[0] && magenta[1] < magenta[2]); } // Once a fraction of Photoshop's strength (Swift's issue #252). void CameraRawTests::whiteBalanceAndVibranceMatchPhotoshop() { struct Case { const char *name; std::array input; std::function adjust; std::array expected; }; const std::vector cases{ {"temperature +100", {128, 128, 128}, [](CameraRawSettings &s) { s.temperature = 100; }, {235, 189, 117}}, {"temperature −50", {128, 128, 128}, [](CameraRawSettings &s) { s.temperature = -50; }, {102, 146, 210}}, {"tint +100", {128, 128, 128}, [](CameraRawSettings &s) { s.tint = 100; }, {159, 125, 215}}, {"tint −100", {128, 128, 128}, [](CameraRawSettings &s) { s.tint = -100; }, {98, 198, 120}}, {"vibrance +100 spares skin", {153, 107, 107}, [](CameraRawSettings &s) { s.vibrance = 100; }, {167, 98, 98}}, {"vibrance +100 fills a dull cyan", {107, 153, 153}, [](CameraRawSettings &s) { s.vibrance = 100; }, {0, 160, 160}}, {"vibrance −100", {92, 92, 230}, [](CameraRawSettings &s) { s.vibrance = -100; }, {164, 164, 210}}, }; for (const Case &test : cases) { CameraRawSettings settings; test.adjust(settings); const std::array got = pixels(settings.apply(image(4, 4, test.input[0] / 255.0, test.input[1] / 255.0, test.input[2] / 255.0)))[0]; for (int c = 0; c < 3; ++c) QVERIFY2(std::abs(got[c] - test.expected[c]) <= 4, test.name); } } // Photoshop's own measured numbers, as Swift's test holds them. void CameraRawTests::mixerCalibrationCurveAndVignettesMatchPhotoshop() { struct Case { const char *name; std::array input; std::function adjust; std::array expected; }; const std::vector cases{ {"orange saturation −60", {230, 115, 0}, [](CameraRawSettings &s) { s.mixer.saturation[1] = -60; }, {189, 130, 107}}, {"red hue +60", {230, 92, 92}, [](CameraRawSettings &s) { s.mixer.hue[0] = 60; }, {227, 125, 83}}, {"blue luminance +60", {92, 92, 230}, [](CameraRawSettings &s) { s.mixer.luminance[5] = 60; }, {142, 142, 248}}, {"blue primary saturation −60", {92, 92, 230}, [](CameraRawSettings &s) { s.calibration.blueSaturation = -60; }, {149, 149, 226}}, {"red primary hue +60", {230, 92, 92}, [](CameraRawSettings &s) { s.calibration.redHue = 60; }, {228, 118, 37}}, {"darks −60", {130, 130, 130}, [](CameraRawSettings &s) { s.curve.darks = -60; }, {101, 101, 101}}, }; for (const Case &test : cases) { CameraRawSettings settings; test.adjust(settings); const std::array got = pixels(settings.apply(image(4, 4, test.input[0] / 255.0, test.input[1] / 255.0, test.input[2] / 255.0)))[0]; for (int c = 0; c < 3; ++c) QVERIFY2(std::abs(got[c] - test.expected[c]) <= 4, test.name); } // Mid gray, 3:2, a corner: the lens lifts, post-crop darkens. const QImage wide = image(300, 200, 128 / 255.0, 128 / 255.0, 128 / 255.0); CameraRawSettings lens; lens.optics.vignetteAmount = 100; QVERIFY(std::abs(pixels(lens.apply(wide))[0][0] - 223) <= 6); CameraRawSettings postCrop; postCrop.vignetteAmount = -60; QVERIFY(std::abs(pixels(postCrop.apply(wide))[0][0] - 30) <= 6); } void CameraRawTests::vibranceFavorsDullColorsAndProtectsSkinWhileSaturationDoesNot() { const QImage dullGreen = image(4, 4, 77 / 255.0, 153 / 255.0, 77 / 255.0), saturatedGreen = image(4, 4, 20 / 255.0, 200 / 255.0, 20 / 255.0); const QImage skin = image(4, 4, 153 / 255.0, 115 / 255.0, 77 / 255.0); CameraRawSettings settings; settings.vibrance = 100; const int dullBefore = chroma(pixels(dullGreen)[0]), saturatedBefore = chroma(pixels(saturatedGreen)[0]), skinBefore = chroma(pixels(skin)[0]); const int dullDelta = chroma(pixels(settings.apply(dullGreen))[0]) - dullBefore; const int saturatedDelta = chroma(pixels(settings.apply(saturatedGreen))[0]) - saturatedBefore; const int skinDelta = chroma(pixels(settings.apply(skin))[0]) - skinBefore; QVERIFY(dullDelta > saturatedDelta + 10); QCOMPARE(dullBefore, skinBefore); QVERIFY(dullDelta > skinDelta + 10); const QImage red = image(4, 4, 160 / 255.0, 120 / 255.0, 120 / 255.0), blue = image(4, 4, 100 / 255.0, 100 / 255.0, 140 / 255.0); settings = CameraRawSettings(); settings.saturation = 100; const double redRatio = double(chroma(pixels(settings.apply(red))[0])) / chroma(pixels(red)[0]); const double blueRatio = double(chroma(pixels(settings.apply(blue))[0])) / chroma(pixels(blue)[0]); // Photoshop doubles chroma in linear light: less in sRGB. QVERIFY2(std::abs(redRatio - 1.85) < 0.15 && std::abs(blueRatio - 1.73) < 0.15, "saturation raises both"); } void CameraRawTests::textureAndClaritySharpenAnEdgeAndLeaveAFlatField() { const QImage flat = gray(); CameraRawSettings settings; settings.texture = 100; settings.clarity = 100; QVERIFY2(pixels(settings.apply(flat)) == pixels(flat), "a flat field has no local contrast"); const QImage edge = step(); const auto original = pixels(edge); settings = CameraRawSettings(); settings.texture = 100; const auto textured = pixels(settings.apply(edge)); QCOMPARE(textured[0][0], original[0][0]); QVERIFY2(textured[8][0] == original[8][0], "texture's fine radius does not reach this far"); QVERIFY(textured[11][0] != original[11][0]); settings = CameraRawSettings(); settings.clarity = 100; const auto clarified = pixels(settings.apply(edge)); QCOMPARE(clarified[0][0], original[0][0]); QVERIFY2(clarified[8][0] != original[8][0], "clarity's wider radius reaches further in"); settings.clarity = -100; const auto softened = pixels(settings.apply(edge)); QVERIFY(std::abs(softened[12][0] - softened[11][0]) < std::abs(original[12][0] - original[11][0])); } void CameraRawTests::dehazeDeepensOrLiftsAndKeepsAlpha() { // A shadow under a pale sky; one colour, no haze. QImage skyAndShadow = BrushRaster::context(10, 10, false); { QPainter painter(&skyAndShadow); painter.fillRect(QRect(0, 0, 10, 5), QColor(200, 195, 190)); painter.fillRect(QRect(0, 5, 10, 5), QColor(30, 30, 30)); } const int shadow = 5 * 10; QCOMPARE(pixels(skyAndShadow)[shadow][0], 30); CameraRawSettings settings; settings.dehaze = 100; QVERIFY(pixels(settings.apply(skyAndShadow))[shadow][0] < 30); settings.dehaze = -100; QVERIFY(pixels(settings.apply(skyAndShadow))[shadow][0] > 30); const QImage pale = image(4, 4, 180 / 255.0, 150 / 255.0, 150 / 255.0); for (const double amount : {100.0, -100.0}) { settings.dehaze = amount; QVERIFY((pixels(settings.apply(pale))[0] == std::array{180, 150, 150, 255})); } const QImage translucent = image(4, 4, 30 / 255.0, 30 / 255.0, 30 / 255.0, 0.5); settings.dehaze = 100; QCOMPARE(pixels(settings.apply(translucent))[0][3], pixels(translucent)[0][3]); } void CameraRawTests::glowIsIdleAtZeroAndHalationFringeIsRedderThanDiffusion() { QImage spot = image(21, 21, 0, 0, 0); for (int y = 8; y < 13; ++y) for (int x = 8; x < 13; ++x) spot.setPixelColor(x, y, Qt::white); CameraRawSettings settings; settings.glowWarmth = 100; settings.glowRange = 100; QVERIFY2(pixels(settings.apply(spot)) == pixels(spot), "range and warmth do nothing until Glow is raised"); settings.glow = 100; settings.glowStyle = CameraRawGlowStyle::diffusion; const auto diffusion = pixels(settings.apply(spot)); settings.glowStyle = CameraRawGlowStyle::halation; const auto halation = pixels(settings.apply(spot)); const size_t fringe = 10 * 21 + 15; QVERIFY(diffusion[fringe][0] > diffusion[0][0]); QVERIFY((halation[0] == std::array{0, 0, 0, 255})); QVERIFY(halation[fringe][0] - halation[fringe][1] > diffusion[fringe][0] - diffusion[fringe][1]); } void CameraRawTests::vignetteDarkensCornersAndHighlightsOnlyWhileDarkening() { const QImage field = gray(9, 9); CameraRawSettings settings; settings.vignetteAmount = -100; const auto darkened = pixels(settings.apply(field)); QVERIFY(std::abs(darkened[4 * 9 + 4][0] - 128) <= 2 && darkened[0][0] < darkened[4 * 9 + 4][0] - 40); const QImage white = image(9, 9, 1, 1, 1); settings.vignetteHighlights = 100; settings.vignetteStyle = CameraRawVignetteStyle::highlightPriority; const int kept = pixels(settings.apply(white))[0][0]; settings.vignetteHighlights = 0; const int exposed = pixels(settings.apply(white))[0][0]; QVERIFY(kept > exposed + 40); settings.vignetteHighlights = 100; settings.vignetteStyle = CameraRawVignetteStyle::paintOverlay; QVERIFY2(pixels(settings.apply(white))[0][0] < kept, "paint overlay does not use Highlights"); settings = CameraRawSettings(); settings.vignetteAmount = 100; const auto plain = pixels(settings.apply(field)); settings.vignetteHighlights = 100; QVERIFY2(plain == pixels(settings.apply(field)), "highlights is idle while the vignette lightens"); } void CameraRawTests::grainIsStableAndTheEffectsEyeDropsTheWholeGroup() { const QImage field = gray(16, 16); CameraRawSettings settings; settings.grainSize = 40; settings.grainRoughness = 80; QVERIFY(pixels(settings.apply(field, std::nullopt, 1, 4)) == pixels(field)); settings.grainAmount = 70; const auto first = pixels(settings.apply(field, std::nullopt, 1, 4)); QVERIFY(first == pixels(settings.apply(field, std::nullopt, 1, 4))); QVERIFY(first != pixels(field)); QVERIFY(first[0][0] == first[0][1] && first[0][1] == first[0][2]); QCOMPARE(pixels(settings.apply(image(4, 4, 0.5, 0.5, 0.5, 0), std::nullopt, 1, 4))[0][3], 0); const QImage edge = step(); settings = CameraRawSettings(); settings.texture = 100; settings.grainAmount = 50; const CameraRawSettings hidden = settings.applying({.effects = false}); QVERIFY(hidden.isIdentity()); QVERIFY(pixels(hidden.apply(edge, std::nullopt, 1, 2)) == pixels(edge)); QVERIFY(pixels(settings.apply(edge, std::nullopt, 1, 2)) != pixels(edge)); } void CameraRawTests::histogramFollowsTheGradeAndClippingPaintStaysOffTheResult() { const QImage black = image(4, 4, 0, 0, 0), white = image(4, 4, 1, 1, 1); QCOMPARE(peakIndex(CameraRawScope::make(black).value().red), 0); QCOMPARE(peakIndex(CameraRawScope::make(white).value().red), 255); CameraRawSettings settings; settings.exposure = 1; QVERIFY(peakIndex(CameraRawScope::make(settings.apply(gray())).value().red) > 128); const std::array shadowed = pixels(CameraRawScope::overlay(black, true, false))[0]; QVERIFY(shadowed[2] > shadowed[0]); const std::array highlighted = pixels(CameraRawScope::overlay(white, false, true))[0]; QVERIFY(highlighted[0] > highlighted[2]); QVERIFY(pixels(CameraRawScope::overlay(black, false, false)) == pixels(black)); const CameraRawScope scope = CameraRawScope::make(image(4, 4, 1, 0, 0)).value(); const auto hottest = std::max_element(scope.vectorscope.begin(), scope.vectorscope.end()) - scope.vectorscope.begin(); QVERIFY2(hottest % CameraRawScope::scopeSide > CameraRawScope::scopeSide / 2, "red sits on the right of the vectorscope"); } void CameraRawTests::curveMixerAndGradingChangeOnlyTheirOwnTones() { const QImage dark = image(4, 4, 0.12, 0.12, 0.12), light = image(4, 4, 0.62, 0.62, 0.62); CameraRawSettings settings; settings.curve.shadows = 100; const int darkGain = pixels(settings.apply(dark))[0][0] - int(std::lround(0.12 * 255)); const int lightGain = pixels(settings.apply(light))[0][0] - int(std::lround(0.62 * 255)); QVERIFY(darkGain > lightGain + 8); settings = CameraRawSettings(); settings.curve.rgb = CameraRawCurveSettings::strongContrast(); QVERIFY(pixels(settings.apply(image(4, 4, 0.25, 0.25, 0.25)))[0][0] < 55); settings = CameraRawSettings(); settings.mixer.hue[0] = 100; const std::array shifted = pixels(settings.apply(image(4, 4, 1, 0, 0)))[0]; QVERIFY(shifted[1] > shifted[2]); settings = CameraRawSettings(); settings.grading.shadows.saturation = 100; const std::array gradedDark = pixels(settings.apply(dark))[0]; const std::array gradedLight = pixels(settings.apply(image(4, 4, 1, 1, 1)))[0]; QVERIFY(gradedDark[0] > gradedDark[1] + 5 && std::abs(gradedLight[0] - gradedLight[1]) <= 2); settings.grading.balance = 100; const std::array balanced = pixels(settings.apply(dark))[0]; QVERIFY(balanced[0] - balanced[1] < gradedDark[0] - gradedDark[1]); settings = CameraRawSettings(); settings.curve.shadows = 100; QVERIFY(pixels(settings.applying({.curve = false}).apply(dark)) == pixels(dark)); } void CameraRawTests::detailSharpeningNoiseAndMaskingPreview() { const QImage edge = step(); CameraRawSettings settings; settings.detail.sharpenAmount = 150; settings.detail.sharpenRadius = 50; QVERIFY(pixels(settings.apply(edge)) != pixels(edge)); // A faint step sharpens too: Masking 0 masks nothing. QImage faint = BrushRaster::context(24, 4, false); for (int y = 0; y < 4; ++y) for (int x = 0; x < 24; ++x) faint.setPixelColor(x, y, x < 12 ? QColor(128, 128, 128) : QColor(136, 136, 136)); const auto sharpened = pixels(settings.apply(faint)); QVERIFY2(sharpened[11][0] == 121 && sharpened[12][0] == 143, qPrintable(QStringLiteral("%1, %2").arg(sharpened[11][0]).arg(sharpened[12][0]))); settings = CameraRawSettings(); settings.detail.noiseLuminance = 80; QVERIFY2(pixels(settings.apply(gray(8, 8))) == pixels(gray(8, 8)), "luminance NR leaves a flat field alone"); settings.detail.sharpenMasking = 50; const auto mask = pixels(settings.apply(edge, std::nullopt, 1, 0, -1, true)); QVERIFY(std::all_of(mask.begin(), mask.end(), [](const std::array &p) { return p[0] == p[1] && p[1] == p[2]; })); QVERIFY(settings.detail.adjusts()); } void CameraRawTests::opticsDistortionDefringeAndDetailEye() { const QImage stepped = checker(); CameraRawSettings settings; settings.optics.distortion = 100; QVERIFY2(pixels(settings.apply(stepped)) != pixels(stepped), "distortion resamples pixels"); const QImage purple = image(4, 4, 0.8, 0.2, 0.9); settings = CameraRawSettings(); settings.optics.purpleAmount = 100; settings.optics.purpleHueLow = 250; settings.optics.purpleHueHigh = 320; QVERIFY(chroma(pixels(settings.apply(purple))[0]) < chroma(pixels(purple)[0])); settings = CameraRawSettings(); settings.optics.removeChromaticAberration = true; QVERIFY(settings.optics.adjusts()); settings = CameraRawSettings(); settings.detail.sharpenAmount = 40; QVERIFY(pixels(settings.applying({.detail = false}).apply(step())) == pixels(step())); } QTEST_GUILESS_MAIN(CameraRawTests) #include "CameraRawTests.moc"