#include "CameraRawFixtures.h" #include "Document/CameraRawTables.h" #include "Document/EditorSession.h" #include "Document/Filters.h" #include // Swift's a8630e4: one table, the brightness once, a sparser scope. using namespace CameraRawFixtures; class CameraRawFoldTests : public QObject { Q_OBJECT private slots: void curveMixerAndGradingFoldIntoTheTable(); void aPointColourOrRefineSaturationKeepsTheCurvePass(); void aKnownBrightnessIsReadBeforeCalibration(); void aSessionMeasuresItsPreviewSourceOnce(); void everyBandIsMeasuredAndCounted(); void calibrationsBlueHueTurnsAndItsSaturationSaturates(); }; namespace { QImage noise(int side = 32) { QImage picture = BrushRaster::context(side, side, false); quint32 state = 11; for (int y = 0; y < side; ++y) for (int x = 0; x < side; ++x) { std::array channel{}; for (int &value : channel) { state = state * 1664525 + 1013904223; value = int(state >> 24); } picture.setPixelColor(x, y, QColor(channel[0], channel[1], channel[2])); } return picture; } QImage kernel(const QImage &picture, const std::vector &table) { QImage copy = picture.copy(); adjust_camera_raw(copy.bits(), size_t(copy.width()), size_t(copy.height()), size_t(copy.bytesPerLine()), table.data(), CameraRawTables::grid, 0); return copy; } CameraRawSettings coloured() { CameraRawSettings settings; settings.exposure = 0.5; settings.curve.rgb = CameraRawCurveSettings::mediumContrast(); settings.curve.red = {{0, 0}, {0.4, 0.5}, {1, 1}}; settings.mixer.saturation[1] = -40; settings.grading.highlights = {30, 40, 10}; return settings; } int farthest(const QImage &left, const QImage &right) { int most = 0; const auto a = pixels(left), b = pixels(right); for (size_t i = 0; i < a.size(); ++i) for (int c = 0; c < 4; ++c) most = std::max(most, std::abs(a[i][size_t(c)] - b[i][size_t(c)])); return most; } } void CameraRawFoldTests::curveMixerAndGradingFoldIntoTheTable() { const QImage picture = noise(); const CameraRawSettings settings = coloured().normalized(); std::vector table = CameraRawTables::compose(CameraRawTables::stages(settings, {})); // The curve, then the Mixer and Grading, over Light's table. const CameraRawCurveSettings &curve = settings.curve; const std::vector tone = curve.toneTable(), red = curve.channelTable(curve.red), green = curve.channelTable(curve.green), blue = curve.channelTable(curve.blue); camera_raw_table_curves(table.data(), CameraRawTables::grid, tone.data(), red.data(), green.data(), blue.data()); std::vector stages = CameraRawTables::mixerStages(settings.mixer); const std::vector graded = CameraRawTables::gradingStages(settings.grading); stages.insert(stages.end(), graded.begin(), graded.end()); camera_raw_compose_onto(table.data(), CameraRawTables::grid, stages.data(), int(stages.size()), CameraRawTables::size); QVERIFY(pixels(settings.apply(picture)) == pixels(kernel(picture, table))); // Without Light's table the fold starts from no change. CameraRawSettings alone; alone.curve.red = settings.curve.red; const std::vector identity = CameraRawTables::identity(); QCOMPARE(identity.size(), size_t(33) * 33 * 33 * 3); QVERIFY(identity[3 * (16 * 33 * 33 + 4 * 33 + 28)] == 0.5f && identity.back() == 1.0f); std::vector curved = identity; const std::vector straight = alone.curve.toneTable(), redOnly = alone.curve.channelTable(alone.curve.red); camera_raw_table_curves(curved.data(), CameraRawTables::grid, straight.data(), redOnly.data(), straight.data(), straight.data()); QVERIFY(alone.foldingColor({}) == curved); QVERIFY(pixels(alone.apply(picture)) == pixels(kernel(picture, curved))); } void CameraRawFoldTests::aPointColourOrRefineSaturationKeepsTheCurvePass() { const QImage picture = noise(); // A point colour that weighs nothing changes no pixel. CameraRawSettings pointed = coloured(); pointed.mixer.points = {CameraRawPointColor{.hue = 200, .saturation = 0.01, .luminance = 0.99}}; const QImage folded = coloured().apply(picture), passed = pointed.apply(picture); QVERIFY(farthest(folded, passed) <= 3 && pixels(folded) != pixels(passed)); // Refine Saturation is the curve pass's alone. CameraRawSettings refined = coloured(); refined.curve.refineSaturation = 30; const CameraRawSettings light = refined.normalized(); QImage direct = kernel(picture, CameraRawTables::compose(CameraRawTables::stages(light, {}))); light.applyCurveColor(direct.bits(), direct.width(), direct.height(), direct.bytesPerLine(), -1); QVERIFY(pixels(refined.apply(picture)) == pixels(direct)); // A point-colour view keeps the pass, with no points too. const CameraRawSettings plain = coloured().normalized(); QImage shown = kernel(picture, CameraRawTables::compose(CameraRawTables::stages(plain, {}))); plain.applyCurveColor(shown.bits(), shown.width(), shown.height(), shown.bytesPerLine(), 0); QVERIFY(pixels(coloured().apply(picture, std::nullopt, 1, 0, 0)) == pixels(shown)); } void CameraRawFoldTests::aKnownBrightnessIsReadBeforeCalibration() { // Darker noise, where Shadows Tint turns many hues. QImage picture = noise(); for (int y = 0; y < 32; ++y) for (int x = 0; x < 32; ++x) { const QColor colour = picture.pixelColor(x, y); picture.setPixelColor(x, y, QColor(colour.red() / 2, colour.green() / 2, colour.blue() / 2)); } CameraRawSettings settings; settings.shadows = 100; settings.calibration.shadowTint = 100; // Measured on the pixels before Shadows Tint changes them. const CameraRawTables::Brightness before = CameraRawTables::brightness(picture); QImage direct = picture.copy(); settings.normalized().applyCalibration(direct.bits(), direct.width(), direct.height(), direct.bytesPerLine()); const CameraRawTables::Brightness after = CameraRawTables::brightness(direct); const QImage expected = kernel(direct, CameraRawTables::compose(CameraRawTables::stages(settings.normalized(), before))); const QImage late = kernel(direct, CameraRawTables::compose(CameraRawTables::stages(settings.normalized(), after))); QVERIFY(pixels(settings.apply(picture)) == pixels(expected)); QVERIFY(pixels(late) != pixels(expected)); // A brightness handed in is used as it is. const CameraRawTables::Brightness dim{60, 60, 60}; const QImage handed = kernel(direct, CameraRawTables::compose(CameraRawTables::stages(settings.normalized(), dim))); QVERIFY(pixels(settings.apply(picture, std::nullopt, 1, 0, -1, false, dim)) == pixels(handed) && pixels(handed) != pixels(expected)); } void CameraRawFoldTests::aSessionMeasuresItsPreviewSourceOnce() { const QImage picture = noise(); EditorSession session; session.createDocument(32, 32); session.insert(ImportedImage(picture, picture, QStringLiteral("Picture"))); session.beginFilter(FilterKind::cameraRaw); const FilterEdit &edit = session.filterEdit().value(); const CameraRawTables::Brightness measured = CameraRawTables::brightness(edit.previewSource); const CameraRawTables::Brightness carried = edit.previewJob().cameraRawBrightness.value(); QVERIFY(carried.brightest == measured.brightest && carried.luminance == measured.luminance && carried.logBrightest == measured.logBrightest); QCOMPARE(edit.previewBrightness().brightest, measured.brightest); session.cancelFilter(); session.beginFilter(FilterKind::gaussianBlur); QVERIFY(!session.filterEdit().value().previewJob().cameraRawBrightness); } // Forty rows of gray: every band's rows reach the sums. void CameraRawFoldTests::everyBandIsMeasuredAndCounted() { QImage ramp = BrushRaster::context(1, 40, false); for (int y = 0; y < 40; ++y) ramp.setPixelColor(0, y, QColor(y * 6, y * 6, y * 6)); // Python's reading of all forty rows. const CameraRawTables::Brightness read = CameraRawTables::brightness(ramp); QVERIFY(std::abs(read.brightest - 139.556468482) < 1e-6 && std::abs(read.logBrightest - 91.779144447) < 1e-6); // Sixteen bands of three rows, every other row within each. const CameraRawScope scope = CameraRawScope::make(ramp).value(); std::array expected{}; for (int band = 0; band < 16; ++band) for (int y = band * 3; y < std::min(40, band * 3 + 3); y += 2) expected[size_t(y * 6)] += 1; for (size_t level = 0; level < 256; ++level) QVERIFY2(scope.red[level] == expected[level] && scope.blue[level] == expected[level], qPrintable(QString::number(level))); } // The blue primary: hue turns the hue, saturation saturates. void CameraRawFoldTests::calibrationsBlueHueTurnsAndItsSaturationSaturates() { const auto hsl = [](const QImage &picture) { const QColor colour = picture.pixelColor(0, 0).toHsl(); return std::array{colour.hslHue(), colour.hslSaturation(), colour.lightness()}; }; QImage blue = BrushRaster::context(1, 1, false); blue.setPixelColor(0, 0, QColor(60, 80, 200)); const std::array before = hsl(blue); QImage turned = blue.copy(), saturated = blue.copy(); adjust_camera_raw_calibration(turned.bits(), 1, 1, size_t(turned.bytesPerLine()), 0, 0, 0, 0, 0, 80, 0, 6); adjust_camera_raw_calibration(saturated.bits(), 1, 1, size_t(saturated.bytesPerLine()), 0, 0, 0, 0, 0, 0, -80, 6); QVERIFY(std::abs(hsl(turned)[0] - before[0]) > 5 && std::abs(hsl(turned)[1] - before[1]) <= 3); QVERIFY(std::abs(hsl(saturated)[0] - before[0]) <= 2 && hsl(saturated)[1] < before[1] - 40); } QTEST_GUILESS_MAIN(CameraRawFoldTests) #include "CameraRawFoldTests.moc"