* LR-PROF-IMPORT: import the user's own DNG camera profiles (.dcp)
profile.import (File > Import Profiles & Presets, drag & drop) now also
reads DNG Camera Profiles: the user's own .dcp files, e.g. made with a
colour-checker tool. They appear in the profile browser and menu under
Camera Profiles with dcp:<camera>-<file> ids; profiles.camera lists them
and profile.deleteCamera / profile.deleteImported remove them. A library
on disk keeps a copy in Profiles/. No profile ships with the app and the
repository holds none: tests build theirs in memory (write_dcp).
lightcraft_raw::dcp parses the file per DNG spec ch. 6 (magic RC in
place of 42; IFD 0 read through the existing DNG colour builders) and
DcpProfile::applied_to puts its matrices, illuminants and look in place
of a raw file's, keeping the shot's white balance, analog balance and
baseline exposure.
Choosing one (develop.profile) replaces the raw file's colour model when
it is decoded, before the starting colour and the white-balance scale
(WbScaleOf) are worked out, so the matrix path takes over from the fitted
JPEG look (CR3/CR2/ARW...) and white balance follows the profile. A
profile without a tone curve of its own (most chart-made ones) keeps the
photo's contrast: the DNG's tone curve, else the tone of the camera-JPEG
fit. The profile travels with SourceRef::File into FileLoader /
PairLoader (new Option<&DcpProfile> argument); a decoded source records
the profile it was made with and is not reused for another. Smart
previews and the web render worker keep the file's own colour; rendered
(non-raw) photos ignore it.
This reverses the documented "Adobe profile formats deliberately
unsupported" stance for user-supplied files only; docs updated.
Split out of #1012.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* LR-PROF-IMPORT: build against upstream main (kelvin_presets, 3-arg loaders)
DcpProfile::applied_to keeps the file's white-balance presets (new
ColorData::kelvin_presets upstream): they are camera facts like as-shot
neutral. Upstream's new FileLoader test closures take the profile argument.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* LR-PROF-IMPORT: hover previews and browser thumbnails develop the hovered camera profile
The loupe's hover preview (preview_job) and the profile browser's thumbnails (variant_job) took
the photo's own decoded source, so hovering an imported camera profile showed the built-in colour
(and hovering a built-in profile while one was applied kept the camera profile's). Both now decode
as the hovered settings develop: the source follows the settings' camera profile, as the main
render does, and a variant's cache key already follows its settings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* LR-PROF-IMPORT: build against upstream main (face-tile test loader and render_bench take the camera profile)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* CI: the catalog's staging-link test module is Unix-only (unused import broke clippy on Windows)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* CI: same-folder export test expects the '/'-joined subfolder (failed on Windows)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* CI: folder-drop tests build widget ids with the Folders tree's '/' paths (failed on Windows)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* LR-PROF-IMPORT: rustfmt after merging main
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* LR-PROF-IMPORT: a real D750 camera profile as a pinned test corpus, checked against Lightroom
dieli's CC0 Nikon D750 ColorChecker set (Calibrite PROFILER dual-illuminant DCP,
matrix-only; the two chart NEFs; Lightroom Classic 15.6 renders with the profile)
is fetched by cargo xtask corpus --download into corpus/d750-colorchecker/, pinned
to commit f37f4054 and by sha256; nothing is committed.
Tests (skip without the corpus): the profile parses as matrix-only with
Cool White Fluorescent + D55; the matrices blend by inverse temperature; the
matrix-only path inverts the colour matrix and adapts to D50; it takes over the
chart NEFs' colour; imported and applied end to end, the charts develop within
mean dE2000 3.84 (flash) / 3.35 (indoor) of Lightroom's renders.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* LR-PROF-IMPORT: a single .dcp file is a camera profile only, not also an unreadable LUT
profile.import read every plain file that was neither a folder nor a .zip as a .cube LUT, so importing
one .dcp imported the camera profile and also reported it as failed ("stream did not contain valid
UTF-8"). Only .cube files are read as LUTs now.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* LR-PROF-IMPORT: review fixes for imported camera profiles
- A library moved or restored elsewhere finds its camera profiles in its own Profiles/ folder.
- A grid thumbnail is cut from the decoded preview only when that preview used the same camera profile.
- Render and source cache keys carry a fingerprint of the profile file, so a deleted or replaced profile never
has its old renders stand in.
- Camera profiles can be favourites, and favourites/recent entries survive reopening the library.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* LR-PROF-IMPORT: smart previews without the camera profile, current-profile source facts, unsupported files reported
CodeRabbit review of 4ef025d9:
- Build Smart Previews develops the proxy without the photo's imported camera profile and never takes a cached
source decoded with it (the denoised picture is kept); before, only the stale-proxy refresh left it out.
- Session::source_info reports decoder facts only from a cached source developed with the camera profile the
photo uses now, else the catalog's.
- profile.import reports a file that is neither .cube, .dcp nor .zip as failed instead of passing over it.
Each with a regression test in tests_dcp.rs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* LR-PROF-IMPORT: the Before side decodes with its own camera profile; a failed profile copy fails that file only
- build_job (and region_job_before through it) picks the Before render's source with the Before
settings, so it never reuses or decodes a source developed with the edit's camera profile.
- profile.import: a .dcp whose copy into the library fails is reported in `failed`; the profiles
imported with it stay imported and are saved, and nothing half-written is left.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* LR-PROF-IMPORT: a cached smart preview stands in while the original is offline
A smart preview carries no camera profile, so with an imported profile the profile check
rejected the cached proxy and every render decoded it again (it gives the same picture). It
now stands in, for its own level and for the thumbnail cut from it, while the original is
known to be offline; otherwise the profile check is as before, so once the original is back
the photo is decoded from it with its profile. Photos without an imported profile keep main's
behaviour. Test `tests_dcp::a_cached_smart_preview_stands_in_while_the_original_is_offline`
(fails without the fix).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* LR-PROF-IMPORT: deleting a camera profile removes the library's copy before forgetting it
profile.deleteCamera now removes the library's copy of the profile file
first. A copy that is already gone is fine; any other error (file in use,
no access) fails the command and keeps the profile listed, so the library
never holds a profile file it no longer lists. Test:
a_profile_whose_copy_cannot_be_deleted_stays.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* LR-PROF-IMPORT: deleting a camera profile never follows `..` out of the profiles folder
A listed path with a `..` component can start with the profiles folder and
still lead out of it; such a path is not treated as the library's copy, so
deleting the profile leaves that file alone. Test:
deleting_a_profile_never_follows_dot_dot_out_of_the_profiles_folder.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* LR-PROF-IMPORT: both tone fits of a DNG see its lens opcodes; no side effect in the profile-tag comparison
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* LR-PROF-IMPORT: a hover preview's key carries the camera profile file's fingerprint
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* LR-PROF-IMPORT: unusable camera profiles not offered; file's WB scale and the profile's tone kept
Review of #1063 (CodeRabbit, outside-diff findings):
- a camera profile whose file is missing or damaged when the library opens
is no longer offered in the profile browser or accepted by develop.profile
(it silently developed with the raw's own colour); the entry stays, so it is
offered again once the file is back.
- a raw on the relative white-balance scale stays relative with a camera
profile (its matrices turned it to Kelvin, so existing edits were read as
absolute temperatures and the catalog and quick previews disagreed).
- with a camera profile its tone curve, then the tone kept for a curve-less
profile, come before the fitted camera look's tone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* LR-PROF-IMPORT: one Camera Profiles group; the web loader keeps a raw's own colour
Review of #1063 (CodeRabbit):
- a LUT group named "Camera Profiles" that didn't sort last made the profile
menu list that group twice; it is now one group, last, with both kinds.
- without web workers a render ran inline and the web loader applied the
selected camera profile, unlike the worker (the documented web behaviour
keeps the raw's own colour); the loader now leaves it out.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* LR-PROF-IMPORT: camera profile files are read at most to the 64 MiB limit
A .dcp larger than the parser accepts was read whole into memory before
being refused. One bounded read (lightcraft_raw::dcp::read_dcp) stops one
byte past the limit and is used for import, reopening a library and the
duplicate check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* LR-PROF-IMPORT: a test that a hostile hue/saturation map header in a camera profile drops the map (FredApps, #1063)
Dimensions far past the data, past u32 products or zero, an entry count of 4 billion values and a
data offset past the end each drop the map while the profile's matrix still reads.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Docs: parity checklist keeps both sides of the merge
* LR-PROF-IMPORT: tests added on main take the file loader's camera-profile argument
* Merge main; a thumbnail test takes the file loader's camera-profile argument
* Docs: one camera colour row, with main's notes and the import note
* Process key tests take the profile salt argument
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
34 KiB
XMP sidecars and interchange
LightCraft keeps its catalog as the source of truth, and can also store each photo's metadata and develop settings in an XMP sidecar next to the original. Sidecars let edits travel with the files (backups, another LightCraft library, other tools), and let LightCraft pick up edits made elsewhere.
Sidecar files
| Name | <stem>.xmp by default (IMG_0001.CR3 → IMG_0001.xmp); <file>.xmp (IMG_0001.CR3.xmp) with naming: "full". Reading accepts either (preferred first) and .XMP. With stem naming, files sharing a stem don't share a sidecar: see Shared names below. |
| Write | photo.saveMetadataToFile {ids?} (Photo ▸ Save Metadata to File, ⌘S), or automatically after every change with library.xmpPreferences {autoWrite: true} (File ▸ Automatically Write Changes into XMP). Slider drags are written once, when the drag ends; undo/redo rewrite the sidecar. An existing sidecar is merged into, never replaced (see Saving into an existing sidecar). Written atomically (temp file, fsync, rename). The result lists written, merged and backups. |
| Read | On import (library.import, the report counts sidecars), and photo.readMetadataFromFile {ids?} (one undo step). Manual rereading uses embedded XMP in raw/DNG and JPEG files when no sidecar exists. A discovered sidecar that cannot be read or parsed is reported in failed, without applying embedded values. photo.readMetadataFromFile never writes the sidecar, even with auto-write on: writing back what it has just read would add lc:settings, which later reads prefer over another app's crs: edits (issue #934). A file arriving in an import is ranked as if it were catalogued already: it reads its own <file>.xmp when another file of its stem (a raw first) owns the shared stem sidecar (issue #1905). |
| Preferences | library.xmpPreferences {autoWrite?, naming?: stem|full}, stored in the library's prefs.json. |
What we write (standard namespaces, so other tools can read the metadata):
| Field | XMP property |
|---|---|
| Rating 0–5 | xmp:Rating |
| Colour label | xmp:Label (Red, Yellow, Green, Blue, Purple) |
| Title / caption / copyright / creator | dc:title / dc:description / dc:rights / dc:creator |
| Copyright status | xmpRights:Marked (True copyrighted, False public domain, absent = unknown) |
| Rights usage terms / copyright info URL | xmpRights:UsageTerms / xmpRights:WebStatement |
| Keywords | dc:subject (names: Places, Italy, Rome) and, when any keyword has levels, lr:hierarchicalSubject (paths: Places|Italy|Rome, with the flat keywords too) |
| Capture time, GPS | exif:DateTimeOriginal, photoshop:DateCreated, exif:GPSLatitude/GPSLongitude |
| Pick / reject flag | lc:flag (pick, reject, none) |
| Location | lc:location |
| Develop settings | lc:settings — our complete DevelopSettings as JSON (exact round trip, incl. masks, spots, crop and the rendering process; settings without process are V1, see process-versions.md) |
lc: is http://ns.lightcraft.app/lc/1.0/.
Reading merges into the catalog with the sidecar winning for every field it states; fields it doesn't state are
kept. The one exception is the capture time: a valid time embedded in the file (EXIF/IPTC) wins. Invalid EXIF
clock values are skipped, allowing DateTimeDigitized, IFD0 DateTime or embedded XMP to supply the time. If the file
has no valid capture time, the sidecar's exif:DateTimeOriginal, else photoshop:DateCreated, else xmp:CreateDate
(ISO 8601) is used — on import (it then also files a copied photo in its date folder) and by
photo.readMetadataFromFile (undoable). An incomplete or malformed XMP timestamp is not projected as a capture time,
remains available in the raw property map, and does not prevent the next valid capture-time property from being used.
xmp:Rating="-1" (the XMP convention for rejected) sets the reject flag. Develop settings come from
lc:settings when present (exact); otherwise from the crs: fields below (approximate).
When foreign crs: settings are only partly transferable, library.import includes per-photo warnings in its
report. Warnings identify unsupported profile names, saved Upright geometry that will be recalculated, and other
unmapped adjustments; they do not change the source packet or become catalog edits.
Manual rereading and undo leave the JPEG original unchanged. Manual rereading also leaves sidecar files unchanged,
even with automatic XMP writing on. With automatic XMP writing off (the default), undo/redo leave sidecars unchanged;
with it enabled, subsequent edits and undo/redo can trigger the existing sidecar writer. photo.allMetadata uses
its already loaded file bytes for embedded XMP, with an existing sidecar taking precedence.
Manual JPEG rereading uses the first standard XMP APP1 before the first SOS (scan) or EOI (end) marker. Scanning is limited to a 16 MiB header span, including SOI, segment headers and skipped payloads, and 4096 marker/fill steps after SOI. Non-APP1 payloads are skipped without loading them. Each APP1 payload must be complete and is at most 65,533 bytes. Header extraction succeeds for the first complete standard XMP packet within both limits, including a packet ending exactly at either limit; otherwise it fails without a whole-file fallback, including on I/O errors or an incomplete APP1 whose available bytes form parseable XMP. A valid JPEG with XMP beyond those limits therefore cannot be manually reread through this path. Extended XMP assembly and packets between scans are not supported by manual JPEG rereading.
Packet completeness is stricter than the shared best-effort JPEG extractor; it is not strict whole-JPEG
validation. A complete early XMP packet can be returned without examining the remaining file. Import and
photo.allMetadata retain their existing extraction paths. Command regressions in
crates/engine/src/tests/xmp.rs cover metadata/undo and sidecar precedence, an over-nested XMP packet and a
truncated APP1 without an XMP signature. Separate reader cases in sidecar::jpeg_header_tests use small
counted readers and injected limits for span/step boundaries, complete versus truncated APP1, early stopping,
short reads and I/O errors.
Saving into an existing sidecar
A sidecar may already hold another application's data — e.g. its crs: develop settings and xmpMM:History — often
the only copy of those edits outside that application's catalog. Saving never drops it:
- LightCraft owns the properties in the table above:
xmp:Rating,xmp:Label,dc:title,dc:description,dc:rights,dc:creator,dc:subject,lr:hierarchicalSubject,Iptc4xmpCore:Location,Iptc4xmpCore:AltTextAccessibility,Iptc4xmpCore:ExtDescrAccessibility,photoshop:City/State/Country,xmpRights:Marked/UsageTerms/WebStatementand everything inlc:. They are replaced on every save, and removed when LightCraft has no value (clearing a title in LightCraft clears it in the file). All of them are read back on import, so the library starts from what the sidecar said. - The capture time (
exif:DateTimeOriginal,photoshop:DateCreated) and GPS (exif:GPSLatitude,exif:GPSLongitude) are replaced only when LightCraft has a value; otherwise the file's stay. - Everything else is kept byte for byte: other namespaces (
crs:,xmpMM:, the rest oflr:, unknown ones),xmp:CreatorTool, comments, the packet wrapper and padding. LightCraft's properties go into onerdf:Descriptionof their own; owned properties written by another application (in attribute or element form) are removed from its description, which otherwise stays as it was. - Ownership follows the namespace URI, not the prefix spelling. A foreign namespace bound to
lc:orxmp:stays foreign: its properties are preserved and do not become LightCraft settings or ratings. Genuine namespace aliases remain interchangeable, including namespace declarations containing XML character references. - A sidecar that can't be read as XMP (not well-formed, not UTF-8, no
rdf:RDF) is first copied to<name>.xmp.bak-<date><time>(-1,-2… if taken — a backup is never overwritten), then replaced. A sidecar that can't be read at all (permissions) is left alone and the save fails.
Keywords are written the way Lightroom Classic and darktable write them, so other applications can search them: names
in dc:subject, paths in lr:hierarchicalSubject (keywords::to_file; read back by keywords::from_file, which gives
the same keywords). No export option applies to a sidecar, so no keyword is left out and no synonym added. Sidecars
from LightCraft 0.13 and older have paths in dc:subject; they still read as paths, and the next save writes them this
way.
Limits: LightCraft doesn't write crs:: the other application's
develop settings stay as that application left them, next to LightCraft's own (lc:settings, which LightCraft reads
first).
Shared names
With stem naming, IMG_0001.CR3 and IMG_0001.JPG map to the same IMG_0001.xmp. When two or more files in the
library share a stem, the stem sidecar belongs to one of them — a raw first, otherwise the first by file name — and
the others write and read <file>.xmp (IMG_0001.JPG.xmp), falling back to the stem sidecar for reading when they
have none. So saving one photo never overwrites the other's metadata (Session::sidecar_naming).
An import ranks the files it is adding the same way, before they are catalogued: a TIFF or JPEG arriving beside a
raw of its stem reads its own <file>.xmp and takes nothing from the raw's IMG_0001.xmp, whether it arrives
with the raw or after it (issue #1905).
Face regions (MWG)
LightCraft reads face, pet, focus and barcode regions in the Metadata Working Group's region schema
(mwg-rs:Regions, http://www.metadataworkinggroup.com/schemas/regions/), as Lightroom, digiKam, Picasa and others
write them, from sidecars and from XMP embedded in the file. Named faces become people in the People view
(LR-LIB-PEOPLE in parity-checklist.md), and every region is drawn as a box in the loupe.
- Read only. LightCraft never writes
mwg-rs:Regions; saving a sidecar keeps another application's regions byte for byte. Removing or resizing a box in the loupe changes the library only. - Areas:
stArea:x/yare the box's centre,w/hits size, normalized to the photo (stArea:unit="pixel"areas are divided bymwg-rs:AppliedToDimensions, and dropped without it). Boxes are clipped to the photo; one that misses it entirely, or has a non-finite, zero or negative size, is skipped (the rest of the list is kept). - Orientation: regions are stored on the upright (EXIF-oriented) photo. A
mwg-rs:Rotationof −π/2, +π/2 or ±π (how Lightroom marks a box given in the sensor's frame for Exif orientation 6, 8 or 3) is turned back into the upright frame; MWG can't say "mirrored", so boxes on mirrored photos (orientations 2, 4, 5, 7) are taken as written. Boxes follow LightCraft's own Rotate Left/Right and flips. - Re-reading: a sidecar that has
mwg-rs:Regions— even with an empty list — replaces the photo's regions, so regions removed in another application go away here too. A sidecar withoutmwg-rs:Regions(an application that doesn't do regions) leaves the photo's regions as they are.
Reading crs: develop fields
Many raw developers store edits as crs: properties (http://ns.adobe.com/camera-raw-settings/1.0/) in sidecars, in
DNG files and in XMP presets. LightCraft reads the common ones and maps them to its own controls. We implemented this
from the public XMP specification and by observing what each field does; no third-party code or preset files were used.
Our pipeline renders differently, so values carry over but the look is approximate.
We read these fields; we never write them. Only fields in the packet are applied: the result is a partial settings
object that gets merged like a preset, so everything else keeps its current or default value. Packets marked
crs:AlreadyApplied="True" are skipped, because those pixels already contain the edit. Only process-version 2012+ field
names are read (e.g. Exposure2012, not the older Exposure). crs:ProcessVersion itself numbers the other
application's renderer and is ignored: the photo keeps LightCraft's own process, the latest for a newly imported photo
(process-versions.md).
crs: field(s) |
LightCraft control | Notes |
|---|---|---|
Exposure2012 |
light.exposure |
EV, 1:1 |
Contrast2012, Highlights2012, Shadows2012, Whites2012, Blacks2012 |
light.contrast … light.blacks |
−100..100, 1:1 |
HDREditMode |
hdr.enabled |
1 / True = HDR editing on, 0 / False off |
WhiteBalance |
wb.mode |
As Shot, Auto, Daylight, Cloudy, Shade, Tungsten, Fluorescent, Flash; other names → custom |
Temperature, Tint |
wb.temp, wb.tint (+ wb.scale: kelvin) |
Kelvin / tint as written for raws with colour matrices — in the file (DNG) or measured for the camera (lightcraft_raw::spectral: ILCE-7M3, D850, EOS 5D Mark III…, issue #730) or read from its white-balance presets (lightcraft_raw::kelvin_presets: ILCA-99M2, ILCA-77M2…) — and for presets; the mapped settings say "scale": "kelvin" so the value stays Kelvin wherever it lands. Raws LightCraft develops relative to their as-shot look (ARW, NEF, RW2, RAF, CR3, CR2, PEF, SRW of other cameras: 6500 K / 0 means as shot) take the same mired shift from the packet's AsShotTemperature / AsShotTint instead; without that reference a custom Kelvin stays As Shot and is reported (Temperature, Tint (custom white balance without AsShotTemperature: kept As Shot)), because read on the relative scale it would be a large colour cast (issue #510). Note that LightCraft's Kelvin for a camera comes from its measured matrices, Lightroom's from Adobe's: the two scales differ (about 7–10 % on the ILCE-7M3), so the number is carried over, not the exact look |
IncrementalTemperature, IncrementalTint |
wb.temp, wb.tint |
rendered files: −100..100 on our relative scale (mired shift around 6500 K, same as the Temp slider); a rendered file with only Temperature shifts from AsShotTemperature like the raws above when it is present |
Vibrance, Saturation |
color.vibrance, color.saturation |
1:1 |
Texture, Clarity2012, Dehaze |
effects.texture, effects.clarity, effects.dehaze |
1:1 |
HueAdjustment<Band>, SaturationAdjustment<Band>, LuminanceAdjustment<Band> |
mixer.<band>.hue/sat/lum |
bands Red, Orange, Yellow, Green, Aqua, Blue, Purple, Magenta |
ConvertToGrayscale |
treatment |
True → B&W |
GrayMixer<Band> |
bw_mix.<band> |
|
ParametricShadows, ParametricDarks, ParametricLights, ParametricHighlights |
curve.shadows/darks/lights/highlights |
|
ParametricShadowSplit, ParametricMidtoneSplit, ParametricHighlightSplit |
curve.split_shadows/split_mid/split_highlights |
|
CurveRefineSaturation |
curve.refine_saturation |
0..100 (100 = curve saturation unchanged) |
ToneCurvePV2012, ToneCurvePV2012Red/Green/Blue |
curve.master/red/green/blue |
"x, y" points in 0..255 → 0..1; a straight 0→255 line = no curve; the red / green / blue curves are read only when ToneCurvePV2012 and all three channel curves are present (Lightroom Classic ignores them otherwise: a master + red-only packet renders without the red curve) |
SplitToningShadowHue/Saturation, SplitToningHighlightHue/Saturation |
grading.shadows/highlights.hue/sat |
|
ColorGradeShadowLum, ColorGradeHighlightLum |
grading.shadows/highlights.lum |
|
ColorGradeMidtoneHue/Sat/Lum, ColorGradeGlobalHue/Sat/Lum |
grading.midtones/global.* |
|
ColorGradeBlending, SplitToningBalance |
grading.blending, grading.balance |
|
Sharpness, SharpenRadius, SharpenDetail, SharpenEdgeMasking |
detail.sharpen_* |
|
LuminanceSmoothing, LuminanceNoiseReductionDetail, LuminanceNoiseReductionContrast |
detail.nr_luminance/nr_detail/nr_contrast |
|
ColorNoiseReduction, ColorNoiseReductionDetail, ColorNoiseReductionSmoothness |
detail.nr_color/nr_color_detail/nr_color_smoothness |
|
PostCropVignetteAmount/Midpoint/Roundness/Feather/HighlightContrast |
vignette.amount/midpoint/roundness/feather/highlights |
|
PostCropVignetteStyle |
vignette.style |
1 highlight priority, 2 colour priority, 3 paint overlay |
GrainAmount, GrainSize, GrainFrequency |
grain.amount, grain.size, grain.roughness |
|
LensProfileEnable, AutoLateralCA |
optics.lens_profile, optics.remove_ca |
lens profiles are the file's own (DNG-embedded) corrections, which Lightroom applies whatever its profile switch says (our inference): only LensProfileEnable=1 carries over, 0 leaves the switch as it is |
LensManualDistortionAmount, VignetteAmount, VignetteMidpoint |
optics.distortion, optics.vignetting, optics.vignetting_midpoint |
|
DefringePurple/GreenAmount/HueLo/HueHi |
optics.defringe_* |
|
ShadowTint, RedHue/Saturation, GreenHue/Saturation, BlueHue/Saturation |
calibration.shadows_tint, calibration.red_hue/red_sat, … |
Calibration panel, 1:1 |
PerspectiveVertical/Horizontal/Rotate/Scale/Aspect/X/Y |
geometry.vertical/horizontal/rotate/scale/aspect/offset_x/offset_y |
|
PerspectiveUpright |
geometry.upright |
0 off, 1 auto, 2 level, 3 vertical, 4 full, 5 guided; saved UprightTransform_* matrices are not imported, so photo import reports that Lightroom geometry was not preserved and the selected mode will be recalculated |
HasCrop, CropLeft/Top/Right/Bottom, CropAngle |
crop.geometry |
normalized edges → rect; angle in degrees; HasCrop="False" → no crop |
Values pass through our control specs, so anything outside our slider ranges gets clamped.
crs:CameraProfile is kept as the selected name in the photo's import warning and the active profile is left as it is: the name carries no rendering data and never selects an Adobe profile. Import reports such a profile as not applied (colours may differ from Lightroom), except Lightroom's default profiles (Adobe Standard, Adobe Color), which every sidecar names and which LightCraft's own starting look stands in for.
Profiles named by presets (Look, issue #1001). A preset keeps the profile it names (crs:Look: Name, Group,
UUID, Amount; in .lrtemplate files the Look table) as profile.look in its settings. When the user has imported
a profile of that name (profile.import; the name compared ignoring case and repeated spaces, the group breaking
ties), applying the preset selects it at the look's amount (Amount 1 = 100 %). Lightroom's basic profiles map to our
own (Adobe Color → Color, Adobe Neutral, Adobe Vivid, Adobe Landscape, Adobe Portrait, Adobe Monochrome
→ Monochrome). Any other Adobe … / Camera … profile and the numbered creative ones (Artistic 01, B&W 05,
Modern 10, Vintage 02) can't be used here, and a third-party profile not imported yet leaves the photo's profile as it
is; import reports either per preset (Profile “Name” (Lightroom's own) / (not imported yet) in unmapped).
References are resolved again whenever profiles are imported or deleted, so a pack's profiles and presets can come in
either order. The look's embedded Parameters are not read.
Not mapped: camera profiles (CameraProfile; we have our own profile set), red eye, lens blur,
process-version 2010 field names, and AI features. Local adjustments and spot removal are read as described below.
Spot removal
Heal and clone spots stored as crs:RetouchAreas (one structure per spot, a mask component inside) and the older
crs:RetouchInfo list (one key = value, … line per spot; read on its own when it is all a packet has) become our
spots (crates/engine/src/crs_masks.rs, spots). Implemented from the public data model and by observing what a
sidecar written for a healed photo contains.
| Field | Ours | Notes |
|---|---|---|
SpotType |
mode |
heal (default) or clone; the removal method (Method) is ours |
Masks → Mask/Ellipse (X, Y, SizeX) |
a circle spot | centre in image fractions; SizeX is a fraction of the width, our size of the long edge |
Masks → Mask/CircularGradient (Top/Left/Bottom/Right) |
a circle spot | the box's centre and half-width |
Masks → Mask/Paint (Dabs, Radius) |
a brushed spot | the dabs are the path |
SourceX, OffsetY |
source_offset |
the source's absolute x and y (despite the name); ours is the offset from the target. Only with SourceState="sourceSetExplicitly" (or no state); anything else leaves the source automatic |
Opacity, Feather |
opacity, feather |
0..1 → 0..100 |
A spot whose mask is any other kind is reported as Spot: <kind>. Sizes are clamped to our spot range. A sidecar's
spots replace the photo's, as its masks do.
Local corrections (masks)
Masks stored as crs: structures are read from sidecars, DNG-embedded XMP, XMP presets and .lrtemplate files
(crates/engine/src/crs_masks.rs). Four containers hold them: MaskGroupBasedCorrections (current) and the older
GradientBasedCorrections, CircularGradientBasedCorrections and PaintBasedCorrections. Each correction becomes one
mask: CorrectionName → name, CorrectionAmount → Amount, inactive corrections are skipped.
| Field | Ours | Notes |
|---|---|---|
LocalExposure2012 |
adjust.exposure |
stored as a fraction, ×4 EV |
LocalContrast2012, LocalHighlights2012, LocalShadows2012, LocalWhites2012, LocalBlacks2012, LocalClarity2012, LocalTexture, LocalDehaze, LocalTemperature, LocalTint, LocalSaturation, LocalHue, LocalSharpness, LocalLuminanceNoise, LocalMoire, LocalDefringe, LocalToningSaturation |
the matching adjust.* |
stored −1..1, ×100 |
LocalToningHue |
adjust.color_hue |
degrees |
other Local* sliders (LocalGrain…) |
— | no counterpart in our masks: reported as Mask: LocalGrain when set (LocalCurveRefineSaturation 100 is neutral) |
Mask/Gradient (FullX/Y, ZeroX/Y) |
linear gradient | full effect at the Full point |
Mask/CircularGradient (Top/Left/Bottom/Right, Angle, Feather, Flipped) |
radial gradient | box fractions → long-edge radii (presets assume 3:2; sidecars use the photo's shape). Flipped="true" means effect inside: in MaskGroupBasedCorrections with Version 2 (what Lightroom writes for an ordinary radial) and in the legacy CircularGradientBasedCorrections Radial Filter, whose default (no Flipped) is outside. A MaskGroupBasedCorrections radial without Version is inside whatever Flipped says, and its Feather is halved (Lightroom rescales it that way when it upgrades one). Inverting is MaskInverted |
Mask/Paint (Dabs "d x y", Radius, Flow, CenterWeight, MaskValue) |
brush | one brush component per correction; MaskValue ≤ 0 erases |
Mask/Image MaskSubType 1 / 2 |
Subject / Sky | other AI selections are reported, not guessed |
Mask/RangeMask Type 2 / 3 (LumRange, DepthRange) |
luminance / depth range | lightness converted to our range scale; a range at full strength at white (third LumRange value 1) stays open above our scene white; colour ranges are reported |
MaskBlendMode 0 / 1 / 2, MaskInverted |
add / subtract / intersect, invert |
Applying a preset adds its masks to the photo's own (masks the photo already has are not added twice), and the Amount
slider scales them. A sidecar's masks replace the photo's, because a sidecar holds the whole edit. Components we can't
carry over are listed in preset.import's unmapped as Mask: <kind>.
Local adjustment presets (mask effects)
mask.importEffects {path | paths, group?, dryRun?} (and preset.import, File ▸ Import Profiles & Presets…, drag & drop)
reads these into the Masking panel's Effect menu (crates/engine/src/cmd/mask_effect_files.rs):
| File | How it is read |
|---|---|
Lightroom Classic .lrtemplate (%APPDATA%\Adobe\Lightroom\Local Adjustment Presets, ~/Library/Application Support/Adobe/Lightroom/Local Adjustment Presets) |
a Lua table with type = "LocalizedAdjustmentPreset"; title (else internalName, else the file name) → name; value holds the Local* field names in lower camel case: exposure2012 → LocalExposure2012, mapped as in the table above (exposure, contrast, clarity are the older process version's; the 2012 fields win); luminanceNoise → Noise, toningHue / toningSaturation → Color |
Camera Raw local-correction .xmp (CameraRaw/LocalCorrections) |
top-level crs:Local* properties (or, in a file with crs:PresetType="Local" without them, its first correction's); crs:Name → name, crs:Group → group. Develop presets (no crs:Local*) stay develop presets |
.lceffect |
ours: {"format": "lightcraft.maskEffects", "version": 2, "effects": [{name, adjust, group?, favorite?, fields?, with_amount?, brush?, extra?}]} (a bare array or one effect also reads); mask.exportEffects {path, names?, group?} writes it |
.zip |
the presets above inside it; its folders give groups |
Groups: a file's folder (relative to the imported folder; names that only say "local adjustment presets" don't count)
names its group, unless the file names one. Lightroom users mark sections of the flat preset menu with divider
presets: presets that set nothing, named like 10 ----- DETAIL ENHANCERS ----- 10 (a run of three or more -, =,
_, *, #, ~, —, .). These become groups instead of effects: a preset whose name starts with a number goes to
the divider with the largest number not above it (12 Sky under 10 ----- DETAIL ENHANCERS ----- 10 → Detail
Enhancers); presets without a number keep their folder's group. An earlier import's divider effects are removed.
Same-named effects are replaced (a star is kept).
Settings with no local slider here are listed per preset in unmapped (and the toast) and kept with the effect in
extra (an .lceffect export carries them), but not applied: grain (Grain; LightCraft has no local grain),
pointColors (Point Color: SrcHue/Sat/Lum, HueShift, SatScale, LumScale, RangeAmount, Hue/Sat/LumRange
{LowerNone, LowerFull, UpperFull, UpperNone}; there is global Point Color only), toningLuminance. Zero values aren't
listed (refineSaturation = 0 in most files). Lightroom keeps brush size, feather, flow and density per brush, not in
presets, so there is nothing to import for them; an effect saved here can carry them (brush).
Applying (mask.applyEffect {name, id? | ids? | all?, photos?: "selected"}) sets the sliders the effect carries —
all of them for an imported preset (unnamed ones to 0, as Lightroom does), or only the ones chosen when it was saved
(mask.saveEffect {fields: "changed" | [..]}) — and Amount only if it carries it. The effect is remembered for the
mask's kind: the next brush / linear / radial / … mask starts with it (mask.toolEffect).
Preset files
Ours: .lcpreset |
JSON {"format": "lightcraft.preset", "version": 1, "presets": [{id, name, group, settings}]}, where settings is a partial develop-settings object (only the groups the preset includes). A file can hold one preset or many, and every preset keeps its group. Import also accepts a bare preset object or an array of them. |
| Export | preset.export {path, ids?, group?}: all user presets by default, or the given ids or one group. In the app: File ▸ Export Presets…, Presets panel ▸ ⋯ ▸ Export User Presets…, or right-click a group ▸ Export Group…. |
| Import | preset.import {paths, group?, dryRun?}: files or folders (recursive): .lcpreset, .xmp, classic .lrtemplate (a Lua table: value.settings holds the same field names as crs:), photos that carry their edits in XMP ("DNG presets" from mobile apps; their crop, geometry and custom white balance are left out) and .zip bundles of any of these. Local adjustment (brush) presets — Lightroom Classic's .lrtemplate with type = "LocalizedAdjustmentPreset", Camera Raw's local-correction .xmp, our .lceffect, also inside zips — become mask effects (mask.effects) instead (see Local adjustment presets below); the result lists them under effects, and divider presets under effectDividers. Presets in a folder (or a folder inside a zip) go to a group named after it, unless the file names its own group. The result lists, per preset, the settings that couldn't be carried over (unmapped, e.g. CameraProfile, a profile named by Look that isn't imported, local masks), and the app's toast names them; fields that only describe the preset or the photo (Cluster, SortName, Description, SupportsAmount2, RequiresRGBTables, CropConstrainToWarp, OverrideLookVignette, AsShotTemperature / AsShotTint, empty PointColors slots) aren't listed. Older (process version 2010) fields — Exposure, Contrast, FillLight, HighlightRecovery, Shadows, Brightness, Clarity, ToneCurve — are approximated with today's sliders when a preset has no 2012-era fields. Dropping preset files on the window imports them too. In the app: File ▸ Import Presets…, or Presets panel ▸ ⋯ ▸ Import Presets…. A preset that's already there (same name, group and settings) is skipped. If an id clashes, the import gets a fresh user.* id, and built-in presets are never replaced. |
| XMP presets | Read with the crs: table above, with crs:Name as the name (falling back to the file name) and crs:Group as the group (falling back to "Imported Presets"). Only the fields the preset sets are included, so applying it leaves everything else alone and the Amount slider scales it like any other preset. We only read XMP presets; we don't write them. |
LightCraft ships no third-party presets. Its built-in presets are its own values (crates/engine/src/presets.rs).
Luminar looks (.lmp, .mplumpack)
preset.import (and drag & drop, File ▸ Import Profiles & Presets…) also reads Luminar looks: an .lmp file is an
XML property list of adjustment layers (each with effects and named sliders, mostly on a −100..100 scale); newer looks
may be a bundle folder Name.lmp/Contents/preset.lmp. An .mplumpack collection is a zip of .lmp files whose
PresetsInfo.plist names the group (GroupName); the pack's file name is the fallback. Looks get their own name
(Name) or their file / bundle name. The sliders with a clear counterpart are carried over; everything else (AI tools,
Orton, glow, LUT layers, colour balance…) is listed in unmapped as Tool.Slider (e.g. OrtonFilter.Amount).
Disabled layers are ignored, layer opacity scales the sliders (and fades curves toward a straight line), and layers
with a blend mode other than Normal or with a mask are reported, not applied. Binary property lists are not read.
| Luminar tool.slider | Ours |
|---|---|
Develop / Light / Exposure Exposure (±100) |
light.exposure (±4 EV — the scale is our reading, not documented) |
Contrast, Highlights, Shadows, Whites, Blacks |
light.* (same scale) |
Temperature, Tint (relative) |
white balance shift as crs:IncrementalTemperature / IncrementalTint; values above 1000 as Kelvin |
Saturation, Vibrance |
color.saturation, color.vibrance |
Clarity Clarity, Structure / AI Structure Amount |
effects.clarity (summed) |
Dehaze Amount |
effects.dehaze |
HSL (MIPLChannelsEffect) h/s/l + Red…Magenta |
mixer.<band>.hue / sat / lum |
Curves RGB / Red / Green / Blue (x, y points in 0..1) |
curve.master / red / green / blue |
Vignette Amount, Vignette Size |
vignette.amount, vignette.midpoint |
Grain Amount |
grain.amount |
Profiles: 3D LUTs (.cube)
profile.import {paths} (File ▸ Import Profiles & Presets…, drag & drop) reads .cube 3D LUTs — single files, folders
or .zip bundles — as creative profiles: they appear in the profile browser under their folder's (or zip's) name and
take the Amount slider (0–200 %) like the built-in looks. The LUT is applied to the finished, display-encoded colour
(trilinear; DOMAIN_MIN / DOMAIN_MAX honoured; 1D LUTs are not supported); photos with a LUT profile render on the
CPU. A library on disk keeps a copy of each file in its Profiles/ folder. XMP camera/creative profiles with embedded
tables are not read.
Profiles: camera profiles (.dcp)
The same command (and File ▸ Import Profiles & Presets…, drag & drop) reads DNG Camera Profiles — the user's own .dcp
files, e.g. made with a colour-checker tool — as camera profiles (dcp:<camera>-<file> ids, group Camera Profiles,
listed by profiles.camera). Read per DNG specification chapter 6 (crates/raw/src/dcp.rs): ProfileName,
UniqueCameraModel, CalibrationIlluminant1/2, ColorMatrix1/2, ForwardMatrix1/2, ProfileHueSatMap*,
ProfileLookTable*, ProfileToneCurve and BaselineExposureOffset. Choosing one (develop.profile {id}) replaces a raw
file's colour model when it is decoded — the file's own white balance, analog balance and baseline exposure stay, and a
profile without a ProfileToneCurve (most chart-made ones) keeps the photo's own contrast: the DNG's tone curve, else the
tone of the look fitted to its camera JPEG — so the
photo's source is decoded again when its profile changes; rendered (non-raw) photos ignore it. Smart previews and the
web build's render worker keep the file's own colour. A library on disk keeps a copy of each file in Profiles/;
profile.deleteCamera (or profile.deleteImported) removes one. No profile ships with LightCraft, and the repository
holds none: tests build theirs in memory (lightcraft_raw::dcp::write_dcp).