diff --git a/AGENTS.md b/AGENTS.md index 0f07c72..5333e89 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -205,7 +205,7 @@ Platform adaptations, each forced by a missing Qt analog: - Ctrl or Meta with a middle drag zooms (14.24, Swift 1.4.9: `e03923f`, Blender's gesture): a middle press with Ctrl or Meta (Swift's Command or Control) keeps `m_middleZoom` (where it began, the zoom then) in place of the middle pan; each move with the middle button zooms by 2^((start.y − y) / 100) about the press, as the Zoom tool's drag doubles every 100 points, up in and down out, under the Zoom tool's magnifier, the minus while below the press; the brush circle hides meanwhile; the release, or a move without the middle button (Qt can lose a release), ends it and puts the tool's cursor back, as the pan's does. A left drag beside it goes on, and a right-button tip drag waits, as beside the pan. Tests: `CanvasNavigationTests::ctrlOrMetaWithTheMiddleButtonZooms` (doubling and halving about an off-centre press, from 75% too, both cursors against the Zoom tool's, Meta, a lost release, a plain middle drag still panning, a Hand drag going on and a tip drag waiting beside it). - Camera Raw's Light and Color as Photoshop's (14.25, Swift 1.4.9: `57ab3bb`): `kernels/AdjustPixels.c/.h` were 57ab3bb's: Exposure, white balance and Contrast ran as one composed table before Highlights and Shadows, Whites, Blacks, Saturation and Vibrance as another after (one table since 14.26). `Document/CameraRawTables.h/.cpp` twin Swift's enum as a namespace: Photoshop's colour tables, 147 of 17³ (`size`), composed over a 33³ `grid`; `tables()` reads them once, `whiteBalance` (four tables round Temperature and Tint, 9 × 9 by 25), `stages` (Swift's order, sliders at zero left out), `compose` (empty without stages, the kernel then handed none; a grid that finds no memory is `ExportError(render)`) and `neutralize` (Swift's search: steps of 10, 2 and 0.5 round the best, the first of equals kept, counted by index where Swift strides, the same values). Swift's bundle resource is a Qt resource compiled into the library (`src/Resources/CameraRawTables.bin`, `qt_add_resources`), so it is always there: Swift's nil-table paths (no tables, no Light and Color, no neutral balance) have no twin, and a header or size that does not match is a `std::logic_error`, a broken build. Its planes were raw DEFLATE until 14.27 (xz since, below). `CameraRawSettings` loses `gains` and its three constants and `neutralizeLinear`; `neutralizeStraight` is the tables' search, and Auto hands it the linear mean encoded again (`encode`), as Swift's. The `Balance` is `CameraRawSettings::Balance`, where Swift's returns a tuple. Tests: Swift's three changed tests in `CameraRawTests` (exposure, translucent too, contrast, whites, blacks, saturation by Photoshop's numbers) and `whiteBalanceAndVibranceMatchPhotoshop`, 181 baked in `CameraRawSessionTests`, `CameraRawTablesTests` (the decoded tables' FNV hash and entries, stages and weights, each slider's tables, past the range, the composed corners, `neutralize`'s exact answers, refinements past −100 on either slider, each channel missing, ties among searched points, three stages composed in order, all from an independent Python decoder and search) (since 14.26 `CameraRawWiringTests::theTableReadsThePicturesBrightness`) and Auto through the encoded mean, a dark pixel on its straight segment. - Adaptive Contrast, Highlights and Shadows; Dehaze, Texture, Clarity and Sharpening (14.26, Swift 1.4.9: `75147a2`): `kernels/AdjustPixels.c/.h` were 75147a2's and the tables its 285 (`CRT2`, 1.4 MB; 763 since 14.27): Contrast, Highlights and Shadows each blend four tables round their value (−100, −50, −25, 0, 25, 50, 100) and the picture's brightness (53, 80, 104, 130, 160, 190, 224 in sRGB levels, `between` over points, held at the ends, the lower pair taking a point whole), read before any stage by `camera_raw_statistics` (`CameraRawTables::brightness`, `Brightness`: the brightest channel's mean for Contrast, the log-average for Highlights, the luminance for Shadows), only when one of the three is set, else 128 each; every Light and Color slider is now one composed table (`stages` returns one list in Camera Raw's order) and `adjust_camera_raw` takes it alone. Dehaze removes or adds the haze colour of the brightest one percent and leaves one colour be; Texture and Clarity are guided filters at radii in proportion to the picture, so the preview's scale no longer reaches them (Glow's still does); Sharpening sharpens everywhere at Masking 0. These live in Swift's kernel, unchanged. The 14.16 tone curve is gone with it, and its curve test. Equivalent mutant: reading the brightness when none of the three is set (the stages then never read it). Tests: Swift's changed `CameraRawTests` (Highlights and Shadows by Photoshop's numbers, Dehaze on a shadow under a pale sky, one colour left be; pinned beside them: Highlights alone reading the picture, Highlights before Whites, a faint step sharpened at Masking 0) and `CameraRawToneOrderTests` (Highlights −100's white), `CameraRawTablesTests` (the 285 tables' hash and entries, two colours' three measures against Python, each slider's tables, the adaptive blends from an independent Python run, held ends and a measured point, three stages composed, mid gray's brightness), `CameraRawWiringTests::theTableReadsThePicturesBrightness` (the grade against the kernel through the picture's own brightness, not the default), `CameraRawRulesTests` (Glow reads the scale, Clarity no longer). -- Camera Raw's Color Mixer, Calibration, curve, vignettes and Color Grading as Photoshop's (14.27, Swift 1.4.9: `bdfb9c2`, `9dac4f1`, `7b5a592`, taken as their net change): `kernels/AdjustPixels.c/.h` are 7b5a592's (a stage holds up to eight tables, `adjust_camera_raw_curve_color` takes the Mixer's and Grading's composed tables, `adjust_camera_raw_optics` Exposure's tables for the lens vignette, the post-crop vignette darkens by a gain in linear light) and the tables its 763 (`CRT3`, 1.25 MB): a header of ten bytes (size, count, `step`), each entry a running difference in steps of `step` levels read as a signed byte and clamped, the planes an xz stream as Foundation's `.lzma` writes it, unpacked through liblzma (`lzma_stream_buffer_decode`, `LibLZMA::LibLZMA`; `liblzma-dev` in the dev image, `xz-devel` and `xz` in the Fedora and Arch images, `xz` in the PKGBUILD and the flake); the app no longer links zlib. `CameraRawTables` gains Swift's `blend` (up to eight parts; a null table is no change), `exposureTables`, `fifty` (−100 to 100 by 50), `gradingStages` (the four luminance sliders, then the global, midtone, highlight and shadow wheels: twelve hues by 30, saturations 25, 50 and 100 below which the lower is no change, the shadow and highlight wheels between Blending 0, 50 and 100, Balance as Swift's fitted shares of saturation, a wheel with no share left out) and `mixerStages` (luminance, saturation, hue, eight colours each); `stages` opens with Calibration's six primaries and ends with the parametric curve's four regions while `CameraRawCurveSettings::hasMeasuredDividers` holds (dividers 25, 50, 75 and Refine Saturation 0), when `toneTable` draws the point curve alone. `applyCurveColor` hands the kernel zeros for the Mixer's and Grading's formulas and their tables in their place; `applyCalibration` draws Shadows Tint alone; `apply` runs the Light table for Calibration or a measured curve too. Equivalent mutants: the shares' floor of zero (a negative share fails the `> 0` test as zero does) and `gradingStages`' `fmod` (a normalized hue lies within 0 to 360, where 360 wraps alike). Tests: Swift's `mixerCalibrationCurveAndVignettesMatchPhotoshop` in `CameraRawTests`; `CameraRawTablesTests` against an independent Python decoder of `CRT3` (the hash and entries; every earlier exact value redone on the stepped tables; the Mixer's, Calibration's, the curve's and Grading's tables and weights, a wheel wrapping past 330, Balance leaving a wheel out, moved dividers); the Highlights pins, Auto's dark pixel and the grading luminance by the same Python; `CameraRawCurveTests` (the tone table as measured and with dividers moved), `CameraRawLensTests` (Shadows Tint by the kernel, primaries through the table), `CameraRawRulesTests` (the clip views keep Calibration's table), `CameraRawWiringTests` (the curve call's tables). +- Camera Raw's Color Mixer, Calibration, curve, vignettes and Color Grading as Photoshop's (14.27, Swift 1.4.9: `bdfb9c2`, `9dac4f1`, `7b5a592`, taken as their net change): `kernels/AdjustPixels.c/.h` are 7b5a592's (a stage holds up to eight tables, `adjust_camera_raw_curve_color` takes the Mixer's and Grading's composed tables, `adjust_camera_raw_optics` Exposure's tables for the lens vignette, the post-crop vignette darkens by a gain in linear light) and the tables its 763 (`CRT3`, 1.25 MB): a header of ten bytes (size, count, `step`), each entry a running difference in steps of `step` levels read as a signed byte and clamped, the planes an xz stream as Foundation's `.lzma` writes it, unpacked through liblzma (`lzma_stream_buffer_decode`, `LibLZMA::LibLZMA`; `liblzma-dev` in the dev image, `xz-devel` and `xz` in the Fedora and Arch images, `xz` in the PKGBUILD and the flake); the app no longer links zlib. `CameraRawTables` gains Swift's `blend` (up to eight parts; a null table is no change), `exposureTables`, `fifty` (−100 to 100 by 50), `gradingStages` (the four luminance sliders, then the global, midtone, highlight and shadow wheels: twelve hues by 30, saturations 25, 50 and 100 below which the lower is no change, the shadow and highlight wheels between Blending 0, 50 and 100, Balance as Swift's fitted shares of saturation, a wheel with no share left out) and `mixerStages` (luminance, saturation, hue, eight colours each); `stages` opens with Calibration's six primaries and ends with the parametric curve's four regions while `CameraRawCurveSettings::hasMeasuredDividers` holds (dividers 25, 50, 75 and Refine Saturation 0), when `toneTable` draws the point curve alone. `applyCurveColor` hands the kernel zeros for the Mixer's and Grading's formulas and their tables in their place, and their serializers (`mixerFloats`, `gradeFloats`), which Swift keeps without a caller, are gone; `applyCalibration` draws Shadows Tint alone; `apply` runs the Light table for Calibration or a measured curve too. Equivalent mutants: the shares' floor of zero (a negative share fails the `> 0` test as zero does) and `gradingStages`' `fmod` (a normalized hue lies within 0 to 360, where 360 wraps alike). Tests: Swift's `mixerCalibrationCurveAndVignettesMatchPhotoshop` in `CameraRawTests`; `CameraRawTablesTests` against an independent Python decoder of `CRT3` (the hash and entries; every earlier exact value redone on the stepped tables; the Mixer's, Calibration's, the curve's and Grading's tables and weights, a wheel wrapping past 330, Blending 0's own tables, Balance +50 doubling the highlights' share and +100 leaving the shadows out, midtones' and highlights' luminance, Calibration, Light and the measured curve in order, moved dividers); the Highlights pins, Auto's dark pixel and the grading luminance by the same Python; `CameraRawCurveTests` (the tone table as measured and with dividers moved), `CameraRawLensTests` (Shadows Tint by the kernel, primaries through the table), `CameraRawRulesTests` (the clip views keep Calibration's table), `CameraRawWiringTests` (the curve call's tables). - Drops between projects: `ProjectWorkspace::draggedLayerSource` finds the tab whose document holds the dragged id (Swift reads the drag pasteboard; the twin reads the drag's `QMimeData`, which every drop target has), `canReceiveDrag(data, destination)` refuses only a layer's own tab, and `receiveProviders` copies several ids one after another, as Swift awaits each provider, so a two-row drop on the New button opens two tabs as Swift's does. `ContentView` takes such a drop over its whole area (`setWorkspace` hands it the workspace; `acceptsDrop` keeps Swift's guards: busy, the sheet, the importer, a rename; `levels` arrives with 9.x), shows the 3-point accent ring inside the canvas while targeted and hands the drop's document point on when it fell on the canvas. `ProjectTabButton` takes drops for its tab (accent fill and rim, "Add to "), and `NewCanvasButton`, the toolbar's New canvas button with the action as its default, for a fresh tab ("Open in a new project tab"), both refusing an Alt drag, which duplicates in the panel; Swift's transient `NewTabDropSlot` in the strip has no twin, the New canvas button is `NewProjectDropTarget` already. Files and pictures arrived with 10.4. - `CanvasThumbnail` draws a layer or a mask inside the canvas's box at `backingScale` 2 (the panel's pixmaps carry that ratio); a mask's ground is `LayerMask::background`, white or black by its edge's majority, as the canvas treats it (Swift's `edgeTone` mean went with 12.12b). `LayerIcons` draws the panel's SF Symbols as paths, in one palette colour, like `ToolIcons`. `BlendModePicker` is a `QComboBox` whose popup previews the highlighted mode through `previewBlendMode` for the layer it opened on (Qt calls `hidePopup` before `activated`, so the preview, the target and the highlight are cleared from the event loop, after a choice had its chance) and refuses a choice once another layer is active. `LayerAppearanceControls` is Swift's blend and opacity row: the slider's press and release are one opacity drag (the release is watched application-wide while the slider is down, so a popup opened mid-drag cannot keep the step open), the percent field applies once through its `FocusOut`, which Return and Escape cause (Swift's `releasesFocusOnCommit`), and steps with Up and Down (Swift's `isFinite` test and the steps' clamp are left out: `setLayerOpacity` refuses a value that is no finite number and clamps the rest), and another active layer makes the controls anew, as Swift's `.id` does. - `ProjectWorkspaceView` is the `QMainWindow`: the front tab's `ContentView` as its central widget, made anew when another tab comes to the front, as SwiftUI's `.id` rebuilds it, and disabled while the workspace manages; the window holds the shown tab (`std::shared_ptr`) until the old editor is deleted, as Swift's views hold their session, so a replaced or closed tab's session never ends under its editor (`ProjectWorkspace::tab` is public for this); a `QToolBar` with the New canvas action, the `ProjectTabStrip` (which takes the toolbar's free width) and the four zoom actions, since SwiftUI's `.toolbar` modifier lives in `ContentView` but the toolbar belongs to the window. Its actions follow the front session's `changed()`, and on that signal the window syncs every tab's button as well, because a busy front tab dims the other tabs' buttons, which their own sessions' signals do not reach; the strip syncs its buttons on the workspace's signal, each button on its own session's. The window's title is the front tab's, with Qt's `[*]` placeholder, so `setWindowModified` can show the mark that `isDocumentEdited` shows; `setWindowFilePath` carries `representedURL`. The editor's own minimum size is cleared inside the window, which owns the 800 × 520 minimum for the whole, toolbar included. Closing the window ignores the event and asks the workspace (`closeWindow`), which confirms tab by tab and then closes the window again with the property `closeConfirmed` set, since the workspace may not include `UI/` headers; a confirmed close is accepted and the mark cleared, so a window shown again asks again. `ProjectTabStrip` is a plain `QWidget` whose buttons are kept for the tabs that stay, made for new tabs and deleted for tabs that went, and placed by hand from `projectTabOverflow` (13.14): every button at its own size hint, so no title is squeezed below Swift's 35 points, laid out again when the strip is resized, the workspace changes or any tab's session changes; it fills no background, so the toolbar shows through (a stylesheet would do the same but freezes the widgets' palette, so a theme switch would pass them by); the unsaved dot is a character before the title. Files named on the command line go to `ProjectWorkspace::receive`, as Finder's Open With reaches the delegate. Layer drops on tabs and the New canvas button are 4.7's; file and picture drops arrived with 10.4. diff --git a/src/Document/CameraRawColor.cpp b/src/Document/CameraRawColor.cpp index 88277e9..948479d 100644 --- a/src/Document/CameraRawColor.cpp +++ b/src/Document/CameraRawColor.cpp @@ -196,15 +196,6 @@ std::array<double, 8> CameraRawMixerSettings::weights(double degrees) return result; } -std::vector<float> CameraRawMixerSettings::mixerFloats() const -{ - std::vector<float> result; - for (const std::array<double, 8> *values : {&hue, &saturation, &luminance}) - for (const double value : *values) - result.push_back(float(value / 100)); - return result; -} - std::vector<float> CameraRawMixerSettings::pointFloats() const { std::vector<float> result; @@ -258,15 +249,6 @@ bool CameraRawGradingSettings::adjusts() const return std::any_of(all.begin(), all.end(), [](const CameraRawGradeWheel &wheel) { return wheel.saturation != 0 || wheel.luminance != 0; }); } -std::vector<float> CameraRawGradingSettings::gradeFloats() const -{ - std::vector<float> result; - for (const CameraRawGradeWheel &wheel : wheels()) - for (const double value : {wheel.hue / 360, wheel.saturation / 100, wheel.luminance / 100}) - result.push_back(float(value)); - return result; -} - CameraRawGradingSettings CameraRawGradingSettings::normalized() const { return {shadows.normalized(), midtones.normalized(), highlights.normalized(), global.normalized(), clamp(blending, 0, 100, 50), diff --git a/src/Document/CameraRawColor.h b/src/Document/CameraRawColor.h index 79f61ee..2bf16c6 100644 --- a/src/Document/CameraRawColor.h +++ b/src/Document/CameraRawColor.h @@ -77,7 +77,6 @@ struct CameraRawMixerSettings { bool adjusts() const; // How much each family shares a hue; neighbours overlap. static std::array<double, 8> weights(double degrees); - std::vector<float> mixerFloats() const; std::vector<float> pointFloats() const; CameraRawMixerSettings normalized() const; friend bool operator==(const CameraRawMixerSettings &, const CameraRawMixerSettings &) = default; @@ -103,7 +102,6 @@ struct CameraRawGradingSettings { double balance = 0; std::array<CameraRawGradeWheel, 4> wheels() const; bool adjusts() const; - std::vector<float> gradeFloats() const; CameraRawGradingSettings normalized() const; friend bool operator==(const CameraRawGradingSettings &, const CameraRawGradingSettings &) = default; }; diff --git a/tests/CameraRawTablesTests.cpp b/tests/CameraRawTablesTests.cpp index a801220..b6220c2 100644 --- a/tests/CameraRawTablesTests.cpp +++ b/tests/CameraRawTablesTests.cpp @@ -166,6 +166,23 @@ void CameraRawTablesTests::theColourGroupsPickTheirTables() CameraRawGradingSettings wrapped; wrapped.global = {345, 50, 0}; QCOMPARE(listed(CameraRawTables::gradingStages(wrapped).at(0), 4), (std::vector<double>{668, 0, 669, 0.5, 635, 0, 636, 0.5})); + // Below Blending 50: the Blending 0 tables, then 50. + CameraRawGradingSettings unblended; + unblended.highlights = {180, 100, 0}; + unblended.blending = 0; + QCOMPARE(listed(CameraRawTables::gradingStages(unblended).at(0), 8), (std::vector<double>{510, 0, 511, 1, 513, 0, 514, 0, 582, 0, 583, 0, 585, 0, 586, 0})); + // Balance +50 doubles the highlight wheel's share. + CameraRawGradingSettings lifted; + lifted.highlights = {30, 20, 0}; + lifted.balance = 50; + QCOMPARE(listed(CameraRawTables::gradingStages(lifted).at(0), 8), + (std::vector<double>{494, 0, 495, 0, 497, 0, 498, 0, 566, 0.4f, 567, 0.6f, 569, 0, 570, 0})); + // Midtones' and highlights' luminance keep their own tables. + CameraRawGradingSettings toned; + toned.midtones.luminance = 60; + toned.highlights.luminance = -40; + const std::vector<CameraRawStage> lumas = CameraRawTables::gradingStages(toned); + QVERIFY(listed(lumas.at(0), 2) == (std::vector<double>{751, 0.8f, 752, 0.2f}) && listed(lumas.at(1), 2) == (std::vector<double>{754, 0.8f, 755, 0.2f})); // Balance +100 leaves the shadow wheel no share at all. CameraRawGradingSettings balanced; balanced.shadows = {200, 40, 0}; @@ -195,6 +212,15 @@ void CameraRawTablesTests::theColourGroupsPickTheirTables() QCOMPARE(listed(stages[4], 2), (std::vector<double>{433, 1, 434, 0})); QCOMPARE(listed(stages[5], 2), (std::vector<double>{435, 0.2f, 436, 0.8f})); QCOMPARE(listed(stages[7], 2), (std::vector<double>{453, 0.5f, 454, 0.5f})); + // Calibration first, Light and Color, then the measured curve. + CameraRawSettings ordered; + ordered.calibration.redHue = 60; + ordered.exposure = 1; + ordered.curve.darks = -60; + const std::vector<CameraRawStage> run = CameraRawTables::stages(ordered, {}); + QCOMPARE(run.size(), size_t(3)); + QVERIFY(listed(run[0], 2) == (std::vector<double>{408, 0.8f, 409, 0.2f}) && listed(run[1], 2) == (std::vector<double>{93, 1, 94, 0})); + QCOMPARE(listed(run[2], 2), (std::vector<double>{440, 0.2f, 441, 0.8f})); // Moved dividers leave the curve to the formula. settings.curve.lightSplit = 70; QCOMPARE(CameraRawTables::stages(settings, {}).size(), size_t(5));