Skip to content

experiment: Implement new raw/jpeg grouping#451

Draft
EnTeQuAk wants to merge 11 commits intoCyberTimon:mainfrom
EnTeQuAk:jpeg-raw-grouping
Draft

experiment: Implement new raw/jpeg grouping#451
EnTeQuAk wants to merge 11 commits intoCyberTimon:mainfrom
EnTeQuAk:jpeg-raw-grouping

Conversation

@EnTeQuAk
Copy link
Copy Markdown

@EnTeQuAk EnTeQuAk commented Nov 14, 2025

I'm using a Fuji camera and regularly shoot JPEG+RAW to have the best of both worlds: straight out of camera JPEGs which are 80% of the time great, and RAWs for special occasions. Seeing every shot twice in the library has always bugged me, so I started experimenting with grouping.

The core idea: Rust assigns a group_id to images that share a stem in the same directory (e.g. DSCF0644.RAF and DSCF0644.JPG), and the frontend collapses each group into a single entry, picking a primary based on your preference. Virtual copies inherit the group but don't count toward it, so a file + its VC alone won't create a false group.

What I implemented:

  • "Group Variants" in the file type filter, with a RAW/JPEG preference toggle underneath
  • A small layers icon on grouped thumbnails; hovering it shows the extensions (e.g. "RAF+JPG")
  • Variant switcher pills in the editor toolbar with actual file extensions, so you see [RAF] [JPG] instead of generic labels
  • Filmstrip stays on the primary when you switch to a non-primary variant, so the position doesn't jump around
Filter dropdown with Group Variants option and RAW/JPEG preference Editor view with variant switcher pills

Along the way I also fixed a pre-existing bug where delete_files_with_associated would grab the wrong stem for filenames with dots (was using split('.').next(), now uses Path::file_stem()), and simplified the hasAssociatedFiles context menu check from O(n*m) string matching to a direct group_id lookup.

Group-aware operations (delete/copy/move/export) aren't in this PR yet. When you delete a grouped file, should it delete all variants? Offer a choice? I'd rather discuss that and build it as a follow-up than guess wrong here.

@EnTeQuAk EnTeQuAk requested a review from CyberTimon as a code owner November 14, 2025 19:40
@CyberTimon
Copy link
Copy Markdown
Owner

Thank you! This is very nice and I can see some use in this. Can you try to match the design of the other components in RapidRAW? (Padding, size etc).

image

Thank you a lot!

As for the associated detection for the delete operations: it currently just checks if the base filename (removing extensions) is identical.

@EnTeQuAk
Copy link
Copy Markdown
Author

Awesome, thanks a lot for your reply. Yeah, the design is far from final. I first need to play around with it more to ensure it's actually useful the way I built it 🙈

As for the associated detection for the delete operations: it currently just checks if the base filename (removing extensions) is identical.

Indeed, I'm currently investigating if it makes sense to make that a bit more explicit and more widely known in other parts of the application.

Right now I'm trying to figure out how to generalize different types of files, e.g, other cameras may have a "XXX + RAW" mode, not sure if JPEG is kinda like the absolute default in the industry.

As I said, it's experimental and will likely take a while to finish. Your feedback is much appreciated :)

@EnTeQuAk EnTeQuAk marked this pull request as draft March 1, 2026 08:42
@da-anda
Copy link
Copy Markdown

da-anda commented Mar 16, 2026

subscribing to this PR since I am very much interested in this (I also shoot in jpeg+raw). This is one thing I am missing after I moved away from Lightroom

split('.').next() returned everything before the first dot, so
'my.photo.2024.ARW' yielded 'my' instead of 'my.photo.2024'. This could
match unrelated files that happened to share a prefix.

Switched to Path::file_stem() which correctly returns everything before
the last dot. Sidecar matching now explicitly handles the
{filename}.rrdata and {filename}.{vc_id}.rrdata naming patterns instead
of relying on the stem shortcut.
list_images_in_dir and list_images_recursive now compute is_raw (via
is_raw_file) and group_id for each file. Files sharing a stem in the
same directory get the same group_id. Virtual copies are excluded from
the count so a file + its VC don't form a false group, but VCs still
inherit the group_id of their source.

Also adds group_associated_files and group_preferred_type to AppSettings
(defaults: off, 'raw').
Add ImageGroup, GroupPreference types and buildImageGroups() utility in
src/utils/imageGrouping.ts. Wire the grouping result into the
sortedImageList pipeline in App.tsx, gated behind the
groupAssociatedFiles setting.

Remove the ~40 lines of RawOverNonRaw stem-matching JS that duplicated
what Rust now provides via group_id. The raw status filter now uses
image.is_raw directly instead of parsing extensions against
supportedTypes.

Settings (groupAssociatedFiles, groupPreferredType) are loaded from
and persisted to AppSettings.
…p fix

Phase 2 + 3 of JPEG+RAW grouping:

Library:
- Thumbnail shows a variant count badge (bottom-right) when the image
  belongs to a group with 2+ variants.
- Filter dropdown: 'Prefer RAW' replaced with 'Group Variants'.
  Selecting it activates grouping (collapsing, badges, switcher).
- Added GroupVariants to RawStatus enum; old RawOverNonRaw values are
  treated as grouping for migration.

Editor:
- EditorToolbar shows variant switcher pills (e.g., [RAF] [JPG]) when
  the current image belongs to a group. Labels use the actual file
  extension, not generic RAW/JPEG.
- Clicking a pill switches to that variant via handleImageSelect.

Filmstrip:
- When the user views a non-primary variant (e.g., switched from JPG
  to RAF), the filmstrip still highlights the group's primary so the
  active position doesn't jump.
- activeDisplayPath prop flows: App -> BottomBar -> Filmstrip.
When 'Group Variants' is selected in the file type filter, a 'Prefer
RAW / JPEG' toggle appears below it. Controls which variant is shown
as the primary in the library grid. Defaults to RAW, persisted via
AppSettings.groupPreferredType.
Replace the O(n*m) startsWith stem-matching with a direct group_id
lookup against the pre-computed groupVariantCounts map.
Grouping is now driven entirely by the rawStatus filter (GroupVariants
or migrated RawOverNonRaw). The separate boolean toggle had no UI and
defaulted to false, so it was dead code.
@EnTeQuAk EnTeQuAk force-pushed the jpeg-raw-grouping branch from aad3ede to cc465bc Compare March 16, 2026 16:24
Three issues found during code review:

1. hasAssociatedFiles relied on groupVariantCounts, which is empty
   when grouping UI is inactive. Since Rust always populates group_id
   for files with shared stems, checking group_id != null is correct
   regardless of the UI state.

2. Virtual copies inherit group_id from their source, so the variant
   switcher pills would appear when editing a VC and point to the
   original files. Suppress the switcher for VCs to avoid silently
   leaving the VC context.

3. getFileExtension had a redundant first lastIndexOf call that was
   only used for an early return before the real logic. Simplified.
@EnTeQuAk
Copy link
Copy Markdown
Author

Alrighty, both of your replies motivated me to pick up the work sooner rather than later 😁

Now, I just pushed a complete rewrite from the original experiment. Given the old branch was 612 commits behind main I reset it and started fresh.

Key differences from the earlier iteration:

  • I moved the grouping entirely to the Rust side of things. The old approach did stem-matching in JS at render time. Now assign_group_ids() runs once during the directory scan, and each ImageFile carries a new group_id as well as is_raw from the backend. The frontend just buckets/groups by group_id
  • replaced RawOverNonRaw entirely. The old "Prefer RAW" filter and the new grouping feature were solving the same problem at different levels. I merged them now into one "Group Variants" filter option. Old saved settings with rawOverNonRaw are migrated automatically.
  • The variant switcher uses real extensions now. Instead of generic "RAW" / "JPEG" labels, the editor pills show the actual extension (RAF, DNG, ARW, CR3, JPG, etc.). The preference toggle in the filter dropdown uses the generic RAW/JPEG labels since those are categories, and a folder can have multiple extensions mixed.
  • Fixed a pre-existing bug in delete_files_with_associated where split('.').next() would grab the wrong stem for filenames with dots. Switched to Path::file_stem() with proper sidecar pattern matching.
  • Simplified hasAssociatedFiles in the context menu. It was doing O(n*m) startsWith calls matching across the full image list. Now it's a direct lookup against the pre-computed group variant counts, which should be much faster.

This pull request still needs group-aware operations (delete/copy/move with confirmation modals), though, I'd like to open that question rather than implement too much right now. What do you think operations like delete/copy/move should behave with the raw/jpeg grouping enabled? I'd build that as a follow-up in a separate pull request, I'd like to focus on the foundation here.

Screenshot_20260316_173303 Screenshot_20260316_173243

@CyberTimon, regarding your earlier feedback: the design now follows the existing component patterns (same padding, border-radius, and color tokens as the rest of the dropdowns and toolbars). The associated files detection for delete uses group_id from the Rust scan rather than the old JS stem-matching, which covers your point about checking base filenames.

Let me know what you think. Happy to hear your feedback on this 👼

@kevinschweikert
Copy link
Copy Markdown

I haven't seen that in other software yet

Photomator has this kind of feature and groups the JPG and RAW file:

image

@EnTeQuAk
Copy link
Copy Markdown
Author

Photomator has this kind of feature and groups the JPG and RAW file:

I stand corrected 👼 but to my excuse, I do not own any Apple hardware 🙈 (aside from the ipad that my kids are using).

I kind of like their more descriptive text on the picture more than my numbered grouping, which is quite ambiguous

image

Do you have the chance to dig deeper on how they're handling it throughout the software, in case you like how they built it?

Swap the variant count number for a compact Layers icon (12px) that
works at any thumbnail size. Tooltip shows the actual extensions
joined (e.g. 'RAF+JPG', 'DNG+ARW+JPG'). Renames groupVariantCounts
to groupBadgeInfo carrying both count and label.
@EnTeQuAk
Copy link
Copy Markdown
Author

I experimented with that idea, but particularly when viewing lots of pictures and scaling the gallery down, it quickly becomes "only text" (next to the image name).

I added an icon to indicate that this picture is grouped, which felt like the best of both worlds 🤔

image

Issues found during code review:

- filmstripActivePath was missing the is_virtual_copy guard that
  variantOptions already had. A VC of a grouped file would highlight
  the group primary in the filmstrip instead of itself.
- pickPrimary switch had no default case. A corrupt settings file
  sending an unknown preference string would return undefined.
- Use Set for extension dedup in groupBadgeInfo (was indexOf loop).
- Add skip_serializing to deprecated group_associated_files so it
  stops being written back to the settings file.
- Remove stale comment.
@EnTeQuAk EnTeQuAk force-pushed the jpeg-raw-grouping branch from 1fea563 to 97fa495 Compare March 16, 2026 19:04
Simplify make_group_key to just path.with_extension(""). Drop
group_associated_files entirely since it never shipped. Introduce
GroupId type alias instead of bare strings. Rename the filter label
to 'Group RAW + JPEG' for clarity. Tighten up comments.
@agirilovich
Copy link
Copy Markdown

This feauture is essential for the migration from other DAM software.
The workflow: "export Lightroom catalog as a collection of RAWs + processed JPEGs", - is described in many other applications documentation. Primary in "How to switch from Lightroom" article.
So, one more vote for the request.

However in this case, having "Prefer JPEG" option is the crucial. Since, imported RAWs are just archived RAWs without any settings and JPEGs should be displayed on a main screen as a result of your job in the past.

I love how this option is implemented in the Zoner Studio, with few more (optional) conditions:
Zoner_grouping
The setting is available "globally" and "per folder". They allow/block grouping if timedate in EXIF is different. And by default, edited JPEGs are not grouped with RAWs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants