The view transform's second curve is the DNG SDK's published reference
rendering — the ACR3 default curve applied by RefBaselineRGBTone — so
it is the "DNG Reference" curve in the panel, D21 and the code, not a
name borrowed from another product. Comments and docs that justified a
choice by another editor doing it ("as their Amount", "so a
photographer arriving from it finds the name") now give the actual
reason. The Vivid presets no longer describe themselves as reaching
for another editor's look; they are DarkRoom's own.
Factual mentions stay: which program wrote the library's DNGs, what
was measured against, and preset import. camera-profiles.md gains §15,
on starting a photograph from the edit it already carries.
389 lines
24 KiB
Markdown
389 lines
24 KiB
Markdown
# Camera profiles — DCP tables on top of the matrix
|
||
|
||
Design for the deferred half of **FR-DEV-3e** ([requirements.md](requirements.md)): the
|
||
`HueSatMap` and `LookTable` of a DNG camera profile, read from the DNG that carries one or from a
|
||
`.dcp` file, and applied after the matrix. Draft of 2026-10-02, recorded as **D20**.
|
||
|
||
---
|
||
|
||
## 1. What we are matching
|
||
|
||
Lightroom renders a raw through a *profile* before any slider moves. A profile is the matrix
|
||
DarkRoom already applies, plus two lookup tables indexed by hue, saturation and value:
|
||
|
||
- **`ProfileHueSatMap`** — a calibration. It corrects what a 3×3 cannot: a sensor whose reds and
|
||
oranges sit in the wrong place relative to its blues, which no linear map fixes. Two copies, one
|
||
per calibration illuminant, interpolated like the matrices.
|
||
- **`ProfileLookTable`** — a rendering intent: hue shifts of up to ±18° by hue, and saturation
|
||
and value scales that vary with brightness. The difference between Adobe Standard, Adobe Color
|
||
and Adobe Vivid is largely this table, together with each profile's tone curve.
|
||
|
||
Without them a raw renders through the matrix alone, which is accurate on a ColorChecker and is
|
||
not what Lightroom showed for the same file.
|
||
|
||
**What the tables do *not* do is make a photograph more saturated.** Measured after the build
|
||
(2026-10-02), on four of the library's 6D DNGs rendered at defaults: Adobe Standard's tables
|
||
*lower* mean saturation by 3–9 %, and the look at 200 % lowers it further. The 6D's look table
|
||
scales saturation by 0.925 in its darkest value rows and by 1.0 from about a fifth of full scale
|
||
up, and its HueSatMap adds about 1 %. Adobe Standard was tuned to sit under Camera Raw's default
|
||
RGB tone curve, which raises saturation in the shadows and midtones, and the look's dark-tone
|
||
desaturation offsets it. Without that curve (§6) the offset is all that is left. On `_MG_9080`, Lightroom 6's own preview
|
||
measures 0.49 mean HSV saturation; the matrix alone renders 0.38, Adobe Standard's tables 0.35.
|
||
That preview also carries whatever was edited in Lightroom, so it is not a clean reference — but
|
||
the direction is unambiguous: **the gap to Lightroom's colour is mostly tone, not the profile's
|
||
tables.** The tables still matter for hue: they are what puts each body's reds, skin and foliage
|
||
where Adobe put them.
|
||
|
||
**What the library holds** (catalog of 2026-10-02): 17,286 of its raws are Canon EOS 6D. The
|
||
9,348 DNGs were written by Lightroom 6.14 and every one sampled embeds *Adobe Standard* with both
|
||
tables — `HueSatMapDims 90 30 1`, `LookTableDims 36 8 16`, `ProfileEmbedPolicy 0` ("allow
|
||
copying"), no `ProfileToneCurve`. The 7,938 CR2s from the same body carry no profile. So the
|
||
tables Lightroom used are already on disk for half the library, and their licence lets them be
|
||
applied to the other half.
|
||
|
||
## 2. The model, as the DNG specification states it
|
||
|
||
Each table is a grid of `(hueShift°, satScale, valScale)` triples over HSV, stored with
|
||
saturation varying fastest, then hue, then value:
|
||
|
||
```
|
||
index = v · (hueDivs · satDivs) + h · satDivs + s
|
||
```
|
||
|
||
Lookup follows the DNG SDK's `RefBaselineHueSatMap`:
|
||
|
||
- **HSV** is the SDK's: `v = max(r,g,b)`, `s = (v − min)/v`, `h ∈ [0, 6)` from which channel
|
||
leads. Grey has `s = 0` and is untouched by construction.
|
||
- **Hue wraps**: `hueDivs` samples over 360°, the last interpolating to the first.
|
||
- **Saturation** samples `0..=1` at `satDivs` points; linear between.
|
||
- **Value** samples `0..=1` at `valDivs` points when `valDivs > 1`; a table with `valDivs = 1` is
|
||
"2.5-D" and ignores value. `ProfileHueSatMapEncoding` / `ProfileLookTableEncoding` = 1 means the
|
||
value axis is indexed by the sRGB-encoded value; 0 (the default, and the 6D's) means linear.
|
||
- **Apply**: `h += hueShift · 6/360`, `s = min(s · satScale, 1)`, `v ·= valScale`; back to RGB.
|
||
- **Space**: linear ProPhoto (ROMM) primaries, D50 white — the space the forward matrix lands in.
|
||
- **Two illuminants**: `HueSatMapData1/2` are interpolated entry by entry, with the same mired
|
||
weight the matrices use. `LookTable` is single.
|
||
|
||
Two departures, both forced by D19's unbounded scene-linear values (the SDK runs these on `[0, 1]`):
|
||
|
||
1. **Value is not clamped.** The SDK writes `min(v · valScale, 1)`; here `v · valScale`, unbounded.
|
||
For lookup only, the value axis reads `min(v, 1)`, so a highlight above 1.0 uses the table's
|
||
brightest row. Where the encoding is sRGB the scale is defined on the encoded value; it is
|
||
applied as the ratio `decode(enc(v′)·valScale)/v′` at `v′ = min(v, 1)`, so a value above 1.0
|
||
gets the brightest row's ratio rather than a clip.
|
||
2. **A colour outside ProPhoto passes through.** A negative component has no SDK HSV. Such a
|
||
colour is outside every surface colour a camera records under normal light. It is left
|
||
unmodified rather than floored, because flooring it clips a value D19 says nothing may clip.
|
||
|
||
## 3. Where it sits
|
||
|
||
```
|
||
… camera matrix ─► vignetting(5) ─► exposure(20) ─► camera_profile(25) ─► contrast(30) ─► … ─► view transform
|
||
│
|
||
working → ProPhoto ─► HueSatMap ─► LookTable ─► ProPhoto → working
|
||
```
|
||
|
||
**A scene operation at order 25, not part of the matrix snippet.** Three reasons:
|
||
|
||
- **The matrix stays what it is.** `cam_to_srgb` is unchanged and still runs where D19 put it, and
|
||
so does every reader of it: the mask pass's copy, the white-balance picker's, the camera-space
|
||
tap. The tables add a conversion into ProPhoto and back *inside* their own fragment, through two
|
||
constant matrices (§3.1). A photograph with no profile composes exactly the shader it does today.
|
||
- **It commutes with what runs before it.** HSV hue and saturation are invariant under a uniform
|
||
gain, and vignetting and exposure are uniform gains. So a 2.5-D table — every Adobe HueSatMap
|
||
seen, and the 6D's — gives the same answer before or after them. That lets one position serve
|
||
both tables, which is where the second reason matters:
|
||
- **The look sees exposure.** The SDK applies `LookTable` after its exposure ramp, so a look that
|
||
desaturates highlights finds the highlights the photographer chose. At 25 it does too. Contrast,
|
||
tone and the colour controls come after it, as they do in the DNG SDK's reference rendering.
|
||
|
||
**Tables are per source, like the matrix.** They are decoded with the raw, interpolated once at
|
||
decode (the HueSatMap blend uses the as-shot neutral, as the matrix does) and carried on
|
||
`DemosaicedImage` next to `color_matrix`. `dr-gpu` uploads them to a storage buffer at
|
||
`@binding(8)` and writes their dimensions into the base uniform block. Every render path that
|
||
reaches `AdjustPass` therefore gets them without being told: develop, export, previews, the tablet.
|
||
A path that had to call a setter on the graph would be a path that one day forgot to, and an export
|
||
that differed from the screen would be the result.
|
||
|
||
### 3.1 The two constants
|
||
|
||
`P⁻¹` is `ColourSpace::ProPhoto.from_linear_srgb()` — the conversion `dr-types` already derives
|
||
from the two spaces' chromaticities, adapting D65 to D50 by Bradford, which the export path uses to
|
||
write ProPhoto files — and `P` is its inverse. The fragment uses `P⁻¹` going in and `P` coming
|
||
out. Each row of both is scaled to sum to one, so working-space white is ProPhoto white exactly and
|
||
a neutral reaches the tables at `s = 0`. For a profile with forward matrices this recovers the
|
||
SDK's ProPhoto colour to within the difference between that derivation and `forward_to_srgb`'s
|
||
published Bradford constants, which is rounding.
|
||
|
||
## 4. Where a profile comes from
|
||
|
||
In this order, first match wins:
|
||
|
||
1. **The profile embedded in the DNG being opened.** It is what the file says, and it was made for
|
||
the matrices the file carries. Read from the root IFD through rawler's parsed `IFD`, as
|
||
`read_dng_matrices` already reads the forward matrices — no second TIFF parser.
|
||
2. **A `.dcp` in the profiles directory** whose `UniqueCameraModel` matches the body — compared
|
||
case-insensitively against the DNG's `UniqueCameraModel` where there is one, and against
|
||
`make + " " + model` otherwise ("Canon EOS 6D"). A DCP is a whole profile: its matrices replace
|
||
the file's, because its tables were built against its forward matrix. The file's as-shot neutral
|
||
is kept. If several match, the first by file name wins, so the choice is stable.
|
||
3. **None.** The matrix alone, as today.
|
||
|
||
The profiles directory is `profiles/` under the platform data directory (`dr_plat::dirs`), loaded
|
||
once per process. A DCP is a TIFF with the magic `IIRC` (0x4352) in place of 42; rawler's
|
||
`GenericTiffReader` already accepts it.
|
||
|
||
**Copying an embedded profile out.** A DNG whose profile has `ProfileEmbedPolicy` 0 ("allow
|
||
copying") or 3 ("no restrictions") can have that profile saved as a `.dcp` into the profiles
|
||
directory. That is how the 6D's CR2s get Adobe Standard: open a 6D DNG, choose *Use this profile
|
||
for every Canon EOS 6D*. Policies 1 ("embed if used") and 2 ("embed never") offer no such action.
|
||
The written file carries the profile's name, copyright and policy unchanged.
|
||
|
||
**Nothing is shipped.** Adobe's profiles are Adobe's; the application ships no `.dcp` and copies
|
||
none on its own. A profile reaches the directory because the photographer put it there or asked
|
||
for it to be copied from their own file.
|
||
|
||
## 5. The control
|
||
|
||
A develop operation, `camera_profile`, `[colour]`, order 25, hand-written (`rust:`) because it
|
||
reads a buffer no declaration can name:
|
||
|
||
| Parameter | Kind | Default | Meaning |
|
||
|---|---|---|---|
|
||
| `apply` | Bool | on | Use the profile's tables, or the matrix alone |
|
||
| `look` | Scalar 0–200 | 100 | Strength of the `LookTable` |
|
||
|
||
`look` scales the look's deltas: `hueShift · a`, `1 + (satScale − 1)·a`, `1 + (valScale − 1)·a`,
|
||
with `a = look/100`, scales floored at 0. At 200 the look is twice as strong, which is the
|
||
"more vivid than Adobe Standard" this started from. The HueSatMap is a calibration and is not
|
||
scaled: `apply` is its only switch.
|
||
|
||
**Always composed while `apply` is on**, as the view transform is: a profile at its defaults *is*
|
||
the rendering, not an edit, so an untouched photograph writes no parameters and still renders
|
||
through its profile. The fragment branches on the uniform that says whether the source has tables,
|
||
so a JPEG, or a raw with none, pays one uniform read. The branch is uniform across the dispatch.
|
||
|
||
**Mask layers.** A layer may offset `look` (blended as a setting, which is linear) but not `apply`;
|
||
the photograph has one profile.
|
||
|
||
**The panel says which profile is in use**, as the lens line does: *Adobe Standard (in the file)*,
|
||
*Adobe Standard (Canon EOS 6D.dcp)*, or *No profile for this camera — matrix only*. The copy-out
|
||
action sits on that line.
|
||
|
||
## 6. Not done, and why
|
||
|
||
- ~~**`ProfileToneCurve` is read and ignored.**~~ *Done after 0.20.0: §12, D21.* It came back as
|
||
an option of the view transform, as this bullet said it would, and that option is the default
|
||
for raws.
|
||
- ~~**`BaselineExposure` is not applied.**~~ *Done after 0.20.0: §11.*
|
||
- **The interpolation follows the as-shot neutral, not the white-balance slider**, as the matrix
|
||
does. Camera Raw re-blends on every temperature change; doing so here means the matrix moves too,
|
||
which is its own change.
|
||
- **Masks select on the matrix's colour.** A colour-range mask sees colour before the profile, as
|
||
it sees colour before every other operation. Deterministic, and a mask is drawn on the picture
|
||
the user sees only approximately anyway.
|
||
- ~~**The profiles directory does not sync.**~~ *Done after 0.20.0: §13.*
|
||
- **Rec.2020 working primaries** stay deferred (D19); nothing here depends on them.
|
||
|
||
## 7. What it costs
|
||
|
||
- **Every DNG with an embedded profile renders differently** — more saturated, which is the point.
|
||
Previews rendered before the change keep the old look until rendered again, as with D19.
|
||
- **Tablet and desktop must be released together.** No schema change, and the sidecar gains only
|
||
ordinary parameters, but two builds render the same DNG differently.
|
||
- **One storage-buffer binding** in every generated shader's layout (a one-entry placeholder when
|
||
there are no tables), and two vec4 slots in the base uniform block.
|
||
- **Per pixel**: two 3×3 multiplies, two HSV round trips, and 4 + 8 buffer reads (bilinear
|
||
HueSatMap, trilinear LookTable). Small next to the fused pass it joins.
|
||
|
||
## 8. Acceptance
|
||
|
||
- **Parsing.** The 6D DNG's embedded profile parses to `90×30×1` and `36×8×16`, its policy to 0,
|
||
its name to "Adobe Standard"; a `.dcp` written from it parses back to the same tables bit for
|
||
bit.
|
||
- **The CPU reference matches the SDK's algorithm**: grey passes through; a table of
|
||
`(0°, 1, 1)` everywhere is the identity to 1e-6; a uniform `satScale` of 1.2 scales HSV
|
||
saturation by 1.2; hue interpolation wraps between the last and first column.
|
||
- **The shader agrees with the CPU reference** on a device, within two 8-bit codes of the
|
||
display-encoded readback (the only readback the adjust pass has), over 256 colours that tables
|
||
of tens of degrees and ±30 % saturation move, and over the library's real Adobe Standard tables
|
||
(`dr-gpu/tests/camera_profile.rs`).
|
||
- **Scene-referred.** A value above 1.0 leaves the stage above 1.0 (`scene_referred_until_the_view`
|
||
covers the operation).
|
||
- **Neutral.** `apply` off renders to the bit what a source with no tables renders.
|
||
- **Two illuminants.** A HueSatMap at blend weight 0 is Data1, at 1 is Data2.
|
||
- **Matching.** An embedded profile beats a directory one; a DCP for "Canon EOS 6D" matches a CR2
|
||
whose rawler make/model is "Canon"/"EOS 6D"; no match leaves the matrix.
|
||
- **Subjective.** A 6D DNG rendered here at defaults is visibly closer to the same file in
|
||
Lightroom 6 with Adobe Standard than the matrix-only render, side by side.
|
||
|
||
## 9. Vivid presets
|
||
|
||
Independent of the tables, and — given §1's measurement — the part of this change that actually
|
||
answers "more colourful". Shipped in the same change: a *Vivid* section of read-only presets
|
||
(`presets/vivid.drpl`) for the "more colourful than the default" request. They use only operations
|
||
every photograph has — vibrance, saturation, the colour mixer, colour grading, contrast — so they
|
||
work on JPEGs and on bodies with no profile, and change only what they name (FR-DEV-6):
|
||
|
||
- **Vivid** — vibrance and a little saturation and contrast: the general-purpose one.
|
||
- **Vivid, strong** — the same, pushed, with deeper blacks.
|
||
- **Vivid landscape** — greens, blues and azure skies, skin bands left alone.
|
||
- **Vivid warm** — oranges and yellows up, a warm highlight cast: golden hour.
|
||
- **Vivid portrait** — vibrance (which protects skin) with the orange and red bands held back.
|
||
|
||
Measured on `_MG_9080` (mean HSV saturation; Lightroom's preview 0.49, DarkRoom's default 0.35):
|
||
Vivid 0.40, Vivid strong 0.44, Vivid landscape 0.46, Vivid warm 0.38, Vivid portrait 0.37 — the
|
||
last two move particular bands, not the whole frame. Rendered with `cargo run --release -p dr-gpu
|
||
--example develop -- FILE.dng out.ppm "preset:Vivid"`.
|
||
|
||
They are bounded by the existing `bundled.rs` tests: every key names a real parameter, every value
|
||
is inside its control's range, and every preset changes something.
|
||
|
||
## 10. Build order
|
||
|
||
1. `dr-decode`: parse the tables (embedded and `.dcp`), the profiles directory, matching, blending,
|
||
the `.dcp` writer. CPU reference of the lookup. Unit tests against the library's 6D DNG,
|
||
skipped when it is absent.
|
||
2. `dr-pipeline`: the `camera_profile` operation, the base-block slots, `@binding(8)`, the WGSL
|
||
lookup; composition tests.
|
||
3. `dr-gpu`: carry the tables on `DemosaicedImage`, upload and bind them; the shader-versus-CPU
|
||
test on a device.
|
||
4. `dr-ui`: profile line, copy-out action, labels; the profiles directory set at start-up on
|
||
desktop and Android.
|
||
5. The *Vivid* presets.
|
||
|
||
---
|
||
|
||
# After 0.20.0: tone, exposure and sync
|
||
|
||
0.20.0 shipped the tables and §1's measurement showed they were not the gap to Lightroom's colour.
|
||
The three items §6 left open are closed here. Draft of 2026-10-03.
|
||
|
||
## 11. Baseline exposure
|
||
|
||
`BaselineExposure` (DNG tag 50730) is the stops a converter adds so a camera's middle grey lands
|
||
where its maker meant it; the 6D's DNGs say +0.25. `BaselineExposureOffset` (51109) is a profile's
|
||
correction to it. The DNG SDK's total is their sum, and so is this one's.
|
||
|
||
- **Applied as a gain on the camera matrix** in `dr-gpu` when a raw is uploaded:
|
||
`cam_to_srgb · 2^total`. A uniform gain commutes with every scene operation before the view
|
||
transform, and the camera-space tap and the white-balance probe read camera RGB before the
|
||
matrix, so neither changes. `RawImage::color_matrix` itself stays the file's: a merge writes a
|
||
linear DNG from it and must not bake a gain into the pixels it also declares in a tag.
|
||
- **A copied profile carries the DNG's baseline.** When an embedded profile is saved as a `.dcp`
|
||
(§4) its `BaselineExposureOffset` is written as the DNG's `BaselineExposure` plus the profile's
|
||
own offset. A CR2 has no baseline of its own, so its total is then the DNG's: the two files of
|
||
one body render at one brightness. The cost: a DNG that embeds no profile, carries its own
|
||
baseline, and matches a copied `.dcp` counts the baseline twice. Every Adobe-written DNG embeds
|
||
its profile, so that DNG is a hand-made one.
|
||
|
||
## 12. DNG reference tone (D21)
|
||
|
||
The DNG SDK's reference rendering runs a raw through the profile's
|
||
`ProfileToneCurve`, or, for a profile that has none — Adobe Standard among them — through the
|
||
*ACR3 default curve*, a 1025-point table published in the DNG SDK and carried by RawTherapee
|
||
(GPLv3) as `adobe_camera_raw_default_curve`.
|
||
|
||
**How it is applied** is half of what it does. The reference does not run the curve on each channel:
|
||
`RefBaselineRGBTone` runs it on the largest and smallest channel and places the middle one at the
|
||
same fraction between them as before. Hue is kept; saturation rises where the curve is steep —
|
||
the shadows and midtones — which is exactly where Adobe Standard's look table desaturated to
|
||
compensate. It runs in linear ProPhoto, on values clipped to `[0, 1]`, and its output is linear.
|
||
|
||
**As the view transform**, not as a stage. D19 has one rendering, last; this is a second kind of
|
||
that rendering, chosen by a new parameter on `view_transform`:
|
||
|
||
| `curve` | What it is | Default for |
|
||
|---|---|---|
|
||
| Sigmoid | D19's log-logistic curve | every raw, for now |
|
||
| DNG reference | the profile's curve, else ACR3, via RGBTone in ProPhoto | — (D21: the default is open) |
|
||
|
||
*Amended 2026-10-03:* the DNG reference was the default in the first draft. The measurement it rested on
|
||
compared against Lightroom renders of *edited* photographs; see D21. The default is decided by
|
||
measuring against Lightroom exports of unedited ones.
|
||
|
||
A JPEG is still not rendered again (FR-DEV-3j). Film simulation still replaces the view transform
|
||
when a stock is chosen.
|
||
|
||
**The two sliders keep meaning something** under the DNG reference curve:
|
||
|
||
- `white` (stops above grey at which the scene reaches display white) sets the input scale:
|
||
`2^(4 − white)`. At its default of 4 the scale is 1 — sensor white is display white, as in
|
||
the SDK's reference.
|
||
- `contrast` bends the input about middle grey before the curve, as a power of
|
||
`contrast / 1.4`: 1 at its default, so the curve is the reference's untouched.
|
||
|
||
**What the ACR curve gives up** is D19's shoulder. Values above display white clip, as they do
|
||
in the SDK's reference; highlight recovery is the highlights slider's job before it. Sigmoid stays one
|
||
click away for a photograph that wants the shoulder.
|
||
|
||
**A profile's own curve** is a list of `(x, y)` pairs. It is resampled at decode onto the same
|
||
1025 points with a natural cubic spline, the DNG SDK's `dng_spline_solver`. A curve that is the
|
||
identity is treated as absent, as RawTherapee does.
|
||
|
||
**On the GPU** the curve rides in the profile buffer (`@binding(8)`) after the tables: a third
|
||
header entry gives its length, then the samples. The placeholder bound for a source without a
|
||
profile carries the ACR3 curve, so a CR2 with no `.dcp` renders through the reference tone when that curve is chosen.
|
||
|
||
## 13. Profiles sync
|
||
|
||
The profiles directory travels with the library, in the server folder that already carries what
|
||
every device must agree on: `<library root>/.darkroom-derived/profiles/`. The scanner excludes
|
||
its parent, as it excludes the trash.
|
||
|
||
- **A step of the derived sync pass** (`derived_sync::run`), after the catalog and before the
|
||
place file, and like the place file it never fails the pass. It lists the server folder and the
|
||
local one; uploads every local `.dcp` the server lacks, or holds at a different size; downloads
|
||
every one the device lacks, reading through a placeholder where the library is a synced folder,
|
||
and writing `.tmp` then renaming so a half-written file is never parsed. If it fetched
|
||
anything, it reloads the profile set.
|
||
- **Files are immutable and named for what they hold** (`<camera> <profile>.dcp`), so a name and a
|
||
size say whether two copies are the same. Two devices that copy the same profile write the same
|
||
name; neither wins over anything.
|
||
- **Not a catalog table.** A schema change stops an older peer merging the catalog at all
|
||
(see the memory of 0.13.3), and a blob of ~120 KB would ride in every catalog upload.
|
||
- **One directory per install**, the union of every library's profiles. A profile describes a
|
||
camera, not a library, so a profile one library brought is right for the same camera in
|
||
another.
|
||
- **Not handled: deleting.** There is no way to remove a profile from the app; a file removed by
|
||
hand on one device comes back from the server on the next pass. A tombstone list is the
|
||
follow-up if deleting is added.
|
||
|
||
## 14. Acceptance for §11–§13
|
||
|
||
- The ACR3 table is 1025 points from 0 to 1, monotone, and matches RawTherapee's values.
|
||
- RGBTone: grey goes through the curve unchanged in hue; a colour keeps its hue (the middle
|
||
channel's fraction between the outer two is unchanged); a curve that is the identity changes
|
||
nothing; the shader agrees with the CPU reference on a device.
|
||
- Sigmoid is the default curve; at its defaults it renders to the bit what 0.20.0 rendered,
|
||
apart from baseline exposure.
|
||
- A DNG with `BaselineExposure` +0.25 renders a flat grey 0.25 EV brighter than the same pixels
|
||
with none; a `.dcp` copied from it carries `BaselineExposureOffset` 0.25 and gives a CR2 the same
|
||
total.
|
||
- A profile resampled from `(0,0) (0.5,0.6) (1,1)` passes through its points.
|
||
- Sync: a `.dcp` present only locally is uploaded; one present only on the server is downloaded,
|
||
parsed and matched on the next decode; a file of the same name and size is left alone.
|
||
- Measured again on `_MG_9080`: mean saturation at defaults closer to Lightroom's 0.49 than 0.20.0's
|
||
0.35.
|
||
|
||
## 15. The photographer's earlier edit
|
||
|
||
What §1 and D21 first took for a difference in rendering is an edit. Every DNG in the library
|
||
carries, in its embedded XMP, the develop settings it was given before it came to DarkRoom — a
|
||
consistent house style: per-colour saturation (blue +58, aqua +50, yellow and purple +23, orange
|
||
+13, green +10), highlights −40, blacks −20, with a second variant (vibrance −10, blue +31). Those
|
||
settings, not the profile and not the tone curve, are why the same photographs looked richer
|
||
before.
|
||
|
||
- **Translated on open** (`dr_preset_xmp::read_embedded`), the HSL bands onto the colour mixer —
|
||
aqua to cyan and purple to violet, the nearest of its twelve by hue — and the rest as the preset
|
||
importer already did.
|
||
- **Applied only to a photograph DarkRoom has no edit of**, and only on positive evidence: no
|
||
sidecar beside a local file, or a server that answered "no such file" with nothing cached
|
||
(`FetchedSidecar::absent`). An edit that failed to arrive is not an absent one, and this would
|
||
otherwise be saved over it.
|
||
- **One undoable step, "Earlier Edit"**, then an ordinary edit, saved with the photograph. Export
|
||
applies it the same way, so a photograph never opened exports as opening it would show.
|
||
- **The translation is one for one for now.** Measurement against the earlier exports (the
|
||
`lr-fit` work) may scale individual bands.
|