RuleSet::problems only checked that a field and operator exist (and,
lately, yes/no values). Any other value was accepted and silently
matched nothing or everything: "Rating ≥ abc", a date of "banana" or
"2026-13", "Color Label isn't pink" (everything), "Title contains ''"
(everything), "in the last 0 days", an album that is gone.
RuleSet::check(&Catalog) -> Vec<Problem> replaces it. Each Problem has
the rule's position (path [1, 0]; shown as "rule 2.1: …"), so the
editor can point at the row. It checks:
- numbers (rating 0–5), shutter speeds (1/250, 2)
- dates: 2026, 2026-08, 2026-08-14 or a full time; real dates only
- between: two bounds; dates in order (2026-08-14 … 2026-08 is fine)
- in the last: N > 0 and a known unit
- choices: the ids, the flag's picked / rejected, a colour label's
custom name (from the catalog), copyright-status spellings
- text and keyword / people rules: something to look for, or "is empty"
- album: exists and is a plain album
- no empty groups (an empty rule list at the top stays "all photos")
album.createSmart, album.setRules and library.filter report every
problem. The editor's date defaults move from "" (which, as "between",
matched every dated photo) to the current year, and its test now
requires every default to be valid, except text and album rules, which
wait for input.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Crash fix. photo.setCaptureTime turned `shift` / `hours` into seconds
with a saturating cast and added them with plain `+`. A shift of 1e30
seconds overflowed. Only the last-resort panic guard caught it,
reporting "failed unexpectedly: attempt to add with overflow". A large
but not overflowing shift reached the catalog as a date like
-114079456599-08-23.
A shift that isn't a number or is more than 10,000 years either way is
now refused up front ("the shift is more than 10,000 years") and
nothing changes. The regression test requires that message, so a
caught panic no longer passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Crash fix. A huge or negative "in the last N" from an agent or a saved
file ({"n": -1e30}) panicked with "attempt to negate with overflow" in
debug builds. Release builds wrapped instead, and the rule matched the
wrong photos.
- last_secs: a zero, negative, NaN or unreadable N is None (the rule
matches nothing, either way round). Spans are capped at 10,000 years,
which means "ever": every dated photo is in it and none is outside it.
- dates::shift_iso: checked_add, and a result outside years 0000–9999
(which iso_seconds can't read back) is None. It never overflows,
whatever the seconds.
Regression tests cover both and panicked before the fix.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Megapixels used the original width × height, while Long Edge, Short
Edge and Aspect Ratio measure the photo as shown (oriented and
cropped). One rule set could disagree with itself: a 24 MP photo
cropped to a 4000 × 4000 square had a long edge of 4000 but still
counted as 24 MP. Megapixels now uses Photo::shown_size too, so that
crop is 16 MP. Rotation doesn't change it.
Behaviour change: a smart album with a Megapixels rule can now leave
out heavily cropped photos it used to include.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With the values now あり / なし, the labels "Has Edits" (編集あり) and
"Has GPS" (GPS情報あり) read "edits: yes … none". They are only
used as smart-album field labels, so they become 編集 and GPS情報 ("編集 …
あり / なし"). The untranslated In a Stack and Cropped become スタック and
切り抜き. A test requires every yes/no field to have a Japanese label
without あり / なし.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
localization.md explains "<context>|<English>" catalog keys and the one
context today (smart album value); localization-ja.md notes あり / なし.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Yes/no rule values used the shared "Yes" / "No" messages, which
Japanese translates as the answers はい / いいえ. As the value of a
condition ("編集あり … いいえ") that reads oddly; あり / なし ("has" /
"has none") is natural.
Messages are keyed by their English text, so the same English word
can't carry two Japanese translations. A small context lookup fixes
that: tr_ctx_in tries "<context>|<source>" first and falls back to the
plain message. Only ja.json adds "smart album value|Yes" / "|No"; the
shared Yes / No (はい / いいえ) stay as they are elsewhere, and every
other language keeps its plain Ja / Sí / Да. A test rejects contextual
keys that name no real value, so a typo can't silently fall back.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From review: the editor rewrote a yes/no value that was neither yes nor
no (written by an agent or an older version) to true on every frame, so
the dialog never showed the problem and OK silently saved "Yes",
turning a rule that matched nothing into one that matches. The dropdown
now shows such a value as written and changes it only when the user
picks Yes or No. Until then the dialog reports the problem and OK
refuses it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From review: album.createSmart and setRules refuse an unknown field, a
missing operator or a yes/no value that is neither yes nor no, but
library.filter applied such a rule set and showed an empty grid. It now
returns the same problems as an error and leaves the current filter in
place.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From review: an agent sending a float (1.0) was refused while 1 was
accepted. Any number equal to 1 or 0 now reads as yes or no; other
numbers (2, 0.5, -1) are still refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Yes/no fields (Has Edits, Cropped, Has GPS, Virtual Copy, In a Stack,
Best of Similar Shots) showed "true" / "false" in the editor and
summaries, and Russian had no translation for either. Matching read the
value with as_bool().unwrap_or(true), so an agent's "false" (a string)
or any other non-boolean counted as yes, while the summary said
"false".
- catalog: bool_value is the one reading. true / "yes" / "true" / 1 /
no value mean yes; false / "no" / "false" / 0 mean no (strings in any
case). Anything else matches nothing, and RuleSet::problems reports it
("`edited` is yes or no, not \"maybe\""), so album.createSmart and
setRules refuse it. bool_label gives "Yes" / "No"; describe() reads
"has edits is yes".
- ui: i18n::bool_text for the editor's dropdown and translated
summaries (Ja / Nein…, existing keys in all locales). The editor
stores the value back as a plain boolean.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From review: the treatment choice "Color" reused the shared "Color" key,
which zh-hans and ru translate as the colour noun (颜色, Цвет) for the
Color panel, giving "Treatment is colour". The colour-label choice
"None" was masculine in Spanish (Ninguno) next to the feminine
"etiqueta". They are now "In Color" and "No Label", translated in all
seven locales (彩色, Цветное, Sin etiqueta…).
Also moves display_label_gaps_are_reported's doc comment back onto it;
the new choice test had been inserted between them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Choice values showed their rule ids in English ("Copyright Status is
publicDomain", "Treatment is monochrome"), and Russian had no
translation for any of them. Kind::Choice now carries (id, label) pairs
like the operators: rules still store and accept the ids, and people
see the labels.
- catalog: choice_label(field, id); RuleSet::describe uses the label
("copyright status is public domain").
- engine: album.ruleFields keeps `choices` as ids and adds
`choiceLabels` {id: label}. A test pins the ids, since the new tuple
type would otherwise serialize as pairs.
- ui: i18n::choice_text is the one path for the editor's value dropdown
and translated summaries. Aspect labels are "Landscape (wide)" /
"Portrait (tall)", because the existing "Landscape" / "Portrait" keys
mean scenery and people. Seven locales gain Video, Copyrighted, Public
Domain, Unknown, the aspect labels and Square (ru also Raw). A test
requires every choice label to be translated in every language.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The field-menu UI test now clicks a group row instead of hovering it,
guarding against the combo popup closing on that click (touch input,
fast clicks). default_values_make_valid_rules now checks each default
value's shape (none, pair, choice, number, a real shutter time) as its
earlier commit message claimed; before, it only checked operators.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From review: `.hidden` reported the extension "hidden", so it matched
"File Extension is hidden". A name whose only dot leads has no extension
now. Photo::is_cropped uses the existing CropGeometry::is_identity
instead of comparing against a default crop.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
parity.md: LRC-LIB-SMARTCOLL lists the 47 fields by menu group and what
Classic has that is still missing. LR-LIB-SMARTALBUM no longer claims
"match-all only, no rule editor". What's New describes the grouped menu
and the new fields.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The any-text rule already searched the city and country but skipped the
state / province. It now also searches the alt text, the people on faces
and a virtual copy's name, so it agrees with the dedicated fields.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New smart-album fields, each in its menu group:
- Source: Copy Name, In a Stack
- File: File Extension (case and leading dot ignored), Video Duration
- Description: Alt Text
- Location: City, State / Province, Country (Location still matches all)
- Size: Long Edge, Short Edge, Aspect Ratio (landscape / portrait /
square within 1 %), measured on the photo as shown (oriented and
cropped)
- Develop: Cropped (crop or straighten), Treatment (color / monochrome)
Photo::extension, Photo::shown_size and Photo::is_cropped hold the
domain rules. A crop rectangle that is NaN or outside the frame is
clamped, so hostile settings can't produce NaN sizes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Camera Info gains Shutter Speed, compared in seconds and written as the
camera shows it: "≤ 1/60" finds faster exposures, "between 1/1000 and
1/125" works, and a value that isn't a time matches nothing. The editor
takes it as text (1/250, 0.5, 2").
The shutter parser moves from engine export into the catalog
(parse_shutter_seconds) so export and the rule share it. A new
rules-editor test checks that every field × operator default value
makes a valid rule.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Smart albums can test how many keywords a photo has ("2 or more", "2 or
fewer", "exactly 2" via the number operators), who is in it (People: the
names on face regions, with the keyword operators) and how many people.
Photo::keyword_count counts a hierarchical keyword once and ignores case
and blank entries; Photo::people is the domain rule for "people in a
photo" (named face regions, trimmed, case-insensitive, pets and unnamed
faces excluded), now shared by the existing person filter. Keyword and
people rules share one name-list matcher.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The rule editor's field dropdown shows Rating, Pick Flag, Color Label and
Any Searchable Text at the top, then a separator and one submenu per
field group instead of one long flat list. The group holding the current
field is shown in bold. Picking a field from a submenu closes the menu
and keeps the operator only when the new field has it. Widget ids
ruleField / ruleFieldGroup / ruleFieldItem let the control channel drive
it, and group names join the i18n display-label gap report.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FIELDS is now listed in field-menu order, with TOP_LEVEL_FIELDS (rating,
pick flag, colour label, any searchable text) followed by FIELD_GROUPS
(Source, File, Date, Keywords & People, Description, Camera Info,
Location, Size, Develop, Assisted Culling). field_group / field_label
look a field up. album.ruleFields reports each field's group so agents
see the same structure as the editor. Rule ids are unchanged, so saved
smart albums keep working.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
std::process::exit skipped eframe's on_exit (ui.json, close_library) and the
may_close check, so changes still in memory could be lost on Cmd+Q.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With a CropAngle, Lightroom's CropLeft/CropTop and CropRight/CropBottom
are the top-left and bottom-right corners of the tilted crop on the
stored image, and its angle turns the other way to LightCraft's. The
import treated the edges as an axis-aligned rectangle in the rotated
frame and kept the angle's sign, so straightened crops came out the
wrong shape and size.
Carry the two corners through the orientation and then LightCraft's
straighten rotation (angle = -CropAngle, mirrored orientations flip it
back), using the stored image's aspect from fileWidth/fileHeight. On a
real catalog every one of 223 straightened crops now matches the frame
of Lightroom's export, and the content difference falls to that of
unstraightened crops.
Refs #483
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
crs crop edges are normalized to the stored (unrotated) image, while
LightCraft's crop rectangle lives in the oriented frame. Catalog import
applied them unchanged, so crops on portrait photos (EXIF 6 or 8) landed
along the wrong axes.
Read Lightroom's orientation code (Adobe_images.orientation: AB, BC, CD,
DA and the mirrored BA, CB, DC, AD) and map the rectangle into the
oriented frame with Orientation::map_norm_rect; a mirrored orientation
also mirrors the straighten angle. The code comes from the catalog, so
this works while the originals are still missing at import time.
Refs #483
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>