hæx.com logo

Fixing CR3 RAW Thumbnails in KDE/Dolphin

Canon switched from CR2 to CR3 (Canon Raw 3) in 2018. Since then, CR3 files show as generic icons in KDE's Dolphin file manager, no thumbnails. CR2 files work perfectly. This seems to affect every recent Canon user on KDE, so me and (mostly) Claude Opus 4.6 tried to fix it.

KDE Bug #489045: https://bugs.kde.org/show_bug.cgi?id=489045

The Rabbit Holes

Before finding the real fix, we chased several leads:

  • MIME magic bytes missing: CR3 had no content-based detection, only filename globs. We added magic bytes (ftypcrx at offset 4) to a local MIME definition. Fixed xdg-mime detection, but thumbnails still didn't appear.
  • Thumbnailer MIME type mismatch: rawthumbnail.so only listed image/x-dcraw, not image/x-canon-cr3. KF6 doesn't resolve MIME subclass relationships. Adding CR3 to the plugin metadata was necessary, but not sufficient alone.
  • KDcraw API too old for CR3?: We suspected KDcraw used LibRaw's old unpack_thumb() which might not work for CR3's ISO BMFF container, and that the newer unpack_thumb_ex() was needed. Turned out unpack_thumb() works fine for CR3. KDcraw never even gets that far.
  • Qt memory allocation limit: Qt's QImageReader has a 256MB limit. Canon R5 RAW decodes to ~267MB. We built a workaround that raised the limit and did full RAW decoding as a fallback. It worked, but was slow, decoding 45 megapixels just to make a thumbnail.

Each of these was a real issue in the pipeline, but none was the actual blocker.

The Final Clue

Renaming a .CR3 file to .CR2 produces an instant thumbnail. Same file contents, different extension, perfect result. That's a suspicious kind of broken.

The Investigation

KDE's thumbnail pipeline for RAW files:

Dolphin -> kio thumbnail worker -> rawthumbnail.so -> KDcraw (libkdcraw) -> LibRaw

Does the thumbnailer plugin know about CR3?

rawthumbnail.so declares it handles image/x-dcraw. CR3's MIME type image/x-canon-cr3 is a subclass of image/x-dcraw in the system MIME database, but KF6's plugin system doesn't resolve MIME subclass relationships. So the thumbnailer is never invoked for CR3. Fix #1: add image/x-canon-cr3 to rawthumbnail.json.

Can LibRaw extract CR3 thumbnails?

Yes. LibRaw 0.21.4 handles CR3 perfectly:

File: r5-craw.CR3 (Canon EOS R5)
Thumb count: 3
  Thumb[0]: 160x120 JPEG (5KB)
  Thumb[1]: 1620x1080 JPEG (68KB)
  Thumb[2]: full-size JPEG (1.19MB)

unpack_thumb() -> SUCCESS (1.19MB JPEG)

All three embedded previews extract instantly. So why does KDcraw fail?

The Root Cause

KDcraw's loadEmbeddedPreview() does this before calling LibRaw:

QString rawFilesExt = QString::fromUtf8(rawFiles());
QString ext = fileInfo.suffix().toUpper();
if (!rawFilesExt.toUpper().contains(ext))
    return false;

It checks the file extension against a hardcoded whitelist in rawfiles.h. That list includes *.cr2 and *.crw, but not *.cr3. The function returns false without ever calling LibRaw. LibRaw would have handled it fine.

This is why renaming .CR3 to .CR2 works, it passes the extension check, then LibRaw opens the file by content and extracts the embedded JPEG preview instantly.

The whitelist hasn't been updated since "Version 5" and is missing CR3 along with potentially other newer formats.

The Fix

Two changes needed across two KDE projects:

1. libkdcraw, Add CR3 to the extension whitelist

In src/rawfiles.h, add "*.cr3 " to the raw_file_extentions[] array. That's it. One line.

2. kdegraphics-thumbnailers, Add CR3 MIME type

In raw/rawthumbnail.json, add "image/x-canon-cr3" to the MimeTypes array, because KF6 doesn't resolve MIME subclass hierarchies.

Result

CR3 thumbnails appear instantly in Dolphin, same speed as CR2, because it's extracting an embedded 1620×1080 JPEG preview rather than decoding 45 megapixels of RAW data.

Workaround (until upstream is patched)

For a local fix that doesn't require patching libkdcraw, you can modify rawcreator.cpp in kdegraphics-thumbnailers to call LibRaw directly as a fallback when KDcraw fails. This bypasses the extension whitelist entirely.

CR3 File Format Details

CR2CR3
ContainerTIFF-basedISO Base Media File Format (like MP4)
Introduced20042018
Magic bytes49 49 2a 00 + 43 52 02 00ftypcrx at offset 4
MIME typeimage/x-canon-cr2image/x-canon-cr3
MIME subclassimage/x-dcraw, image/tiffimage/x-dcraw
Embedded previewsYes (TIFF-accessible)Yes (ISO BMFF boxes: THMB, PRVW)
LibRaw supportFullFull (since ~2019)
KDcraw supportWorksBlocked by extension whitelist

Affected Cameras

Every Canon camera since 2018 that shoots CR3:

  • EOS R, RP, R3, R5, R5 II, R6, R6 II, R7, R8, R10, R50, R100
  • EOS 90D, 250D, 850D
  • EOS M50, M50 II, M6 II, M200
  • PowerShot G5 X II, G7 X III

System Info

  • Kubuntu 25.10
  • KDE Plasma 6 / KF6
  • LibRaw 0.21.4
  • kdegraphics-thumbnailers 25.08.1
  • libkdcrawqt6 25.08.1

The source, open or closed?

It seems to me that one of the reasonable paths towards utopia that has merit is humanity leaning strongly towards open-source "free" information technology. That also goes for just information, but that's another story.

After seeing how Veritasium layed out the XZ attack, I got to thinking. I remember the week when CVE-2024-3094 got out, this top of the scale 10/10 CRITICAL. I was shocked how there was no shock in the news.

If this happens in open source, we should expect it happens in closed source, and a lot more of it, viruses and dark stuff loves the .. dark, closed, places.

There are so many steps between committing code and actually getting through testing, rcs, alphas, betas, etc, this is the perfect example of why open source works, because everyone who cares, does care, and some tests will pick up unexpected changes.

Imagine if there was even more incentive for these open-source people to make good code, they (we) care. A lot.

On the other hand, in the closed industry, this may (and) happen all the time, continuously, we just don't know, because we can't know. 🤷 Until of course, 5 or 10 years, after something crazy happens.

https://github.com/advisories/GHSA-rxwq-x6h5-x525

https://nvd.nist.gov/vuln/detail/CVE-2024-3094

https://en.wikipedia.org/wiki/XZ_Utils_backdoor

https://github.com/tukaani-project/xz/commit/e93e13c8b3bec925c56e0c0b675d8000a0f7f754

Patterns

Math: patterns
Physics: patterns per measurement
Chemistry: patterns per bond
Biology: patterns per survival
Ecology: patterns per system
Neuroscience: patterns per substrate per survival
Psychology: patterns per context per consciousness
Sociology: patterns per context per consciousness per collective
Philosophy: patterns?

Launching Apps from a Bash Script the Way(land) KDE Does

Kubuntu 25.10 · KDE Plasma 6.4.5 · systemd 257

I have a small bash script that opens one of my dev environments; Dolphin on my project assets, Unity Hub, VS Code, and a Konsole in the project directory. It works, but two things always annoyed me:

  1. The script lingers in System Monitor as a phantom process doing nothing, with no visible window.
  2. Dolphin opens as "dolphin" (lowercase, no icon) instead of "Dolphin" with its proper icon, like it would from the KDE start menu. I wondered why.

Time for some Claude Code. Fixing this turned out to be a deep dive into how KDE Plasma actually launches applications.

The original script

#!/bin/bash
PPATH=/home/stig/Projects/Lone/Lone
kstart dolphin "$PPATH/Assets" &
kstart /opt/unityhub/unityhub &
kstart code $PPATH &
konsole --workdir="$PPATH" &

Simple enough. kstart is a KDE utility for launching windowed apps. But it's a holdover from Plasma 5 that doesn't integrate with the way Plasma 6 tracks applications.

Attempt 1: Just drop kstart

#!/bin/bash
PPATH=/home/stig/Projects/Lone/Lone

dolphin "$PPATH/Assets" &>/dev/null &
/opt/unityhub/unityhub &>/dev/null &
code "$PPATH" &>/dev/null &
konsole --workdir="$PPATH" &>/dev/null &

disown -a

This fixed the Dolphin icon issue (the app now sets its own _KDE_NET_WM_DESKTOP_FILE window property), but the apps vanished entirely from System Monitor's Applications tab. The script itself still lingered.

The problem: KDE System Monitor's "Applications" view doesn't use window properties. It uses systemd cgroups.

How KDE tracks applications

When you launch an app from KDE's start menu or KRunner, the launcher doesn't just fork a process. It creates a systemd scope; a named cgroup container; following a specific naming convention:

app-org.kde.dolphin-72323.scope
app-org.kde.konsole-81510.scope

You can see your active scopes with:

systemctl --user list-units --type=scope 'app-*'

KDE System Monitor (specifically libksysguard) parses these scope names with a regex to extract the desktop file ID, then looks up the .desktop file to get the app name and icon. No scope, no entry in the Applications tab.

Attempt 2: systemd-run --scope

#!/bin/bash
PPATH=/home/stig/Projects/Lone/Lone

systemd-run --user --scope -d --unit="app-org.kde.dolphin-$RANDOM.scope" \
    -- dolphin "$PPATH/Assets" 2>/dev/null
systemd-run --user --scope -d --unit="app-unityhub-$RANDOM.scope" \
    -- /opt/unityhub/unityhub 2>/dev/null
systemd-run --user --scope -d --unit="app-code-$RANDOM.scope" \
    -- code "$PPATH" 2>/dev/null
systemd-run --user --scope -d --unit="app-org.kde.konsole-$RANDOM.scope" \
    -- konsole --workdir="$PPATH" 2>/dev/null

This seemed right, but systemd-run --scope acts as the scope lifecycle manager; it blocks, keeping the scope alive as long as the app runs. Only Dolphin would open; the script would hang on line 1 waiting for Dolphin to exit. Running the script a second time would open the remaining three apps (because Dolphin, a single-instance app, would return immediately).

Worse: when I killed the lingering script from System Monitor, it took Unity Hub down with it; because killing the systemd-run process tears down the scope.

System Monitor showing the script lingering alongside properly launched Dolphin, Konsole, and VS Code
After attempt 2: apps show correctly, but the script still lingers and is entangled with the app processes.

The solution: D-Bus scope creation

Looking at how KDE's own launcher works (in KIO::ScopedProcessRunner), the actual mechanism is:

  1. Start the process normally (fork)
  2. Call systemd's StartTransientUnit over D-Bus to create a scope
  3. Pass the PID to move it into the new scope

The scope is created by systemd and persists independently; no lifecycle manager needed. We can do the same from bash with busctl:

#!/bin/bash
PPATH=/home/stig/Projects/Lone/Lone

launch() {
    local id="$1"
    shift
    "$@" &>/dev/null &
    local pid=$!
    busctl call --user org.freedesktop.systemd1 /org/freedesktop/systemd1 \
        org.freedesktop.systemd1.Manager StartTransientUnit \
        'ssa(sv)a(sa(sv))' \
        "app-${id}-${pid}.scope" "fail" \
        1 "PIDs" "au" 1 "$pid" \
        0 &>/dev/null
    disown "$pid" 2>/dev/null
}

launch org.kde.dolphin dolphin "$PPATH"
launch unityhub /opt/unityhub/unityhub
launch code code "$PPATH"
launch org.kde.konsole konsole --workdir="$PPATH"

The launch function:

  1. Starts the app in the background
  2. Captures its PID
  3. Calls StartTransientUnit via D-Bus, creating a scope named app-<desktop-id>-<pid>.scope and moving the process into it
  4. Disowns the process from the shell

Each app now lives in its own cgroup scope. The script's cgroup becomes empty when it exits, so it vanishes from System Monitor. All apps show up with proper names and icons.

After the fix: all apps appear correctly in KDE System Monitor, and the script is gone.

A nice side effect: re-running the script doesn't re-open single-instance apps (like Dolphin), while Konsole opens a new window each time; which is exactly the behaviour I wanted.

Verifying against KDE source

To make sure this approach is stable and won't need migrating, I checked the actual KDE source code.

KIO's ScopedProcessRunner (scopedprocessrunner.cpp) does the same thing: fork the process, then call StartTransientUnit with the PIDs property. KDE uses a UUID for the scope suffix instead of PID, and sets three additional properties:

PropertyValuePurpose
PIDsProcess IDMove process into scope
Sliceapp.sliceProper cgroup hierarchy
DescriptionApp nameHuman-readable in systemctl
SourcePathPath to .desktop fileFallback for desktop file lookup

libksysguard's cgroup.cpp parses the scope name with this regex:

(apps|app|flatpak|dbus)-(?:[^-]*-)?([^-]+(?=-.*\.scope)|[^@]+(?=(?:@.*)?\\.service|.slice))

It extracts the desktop file ID from between app- and -<suffix>.scope, then calls KService::serviceByMenuId() to find the .desktop file. This is why the naming convention matters; use the desktop file's stem (e.g., org.kde.dolphin) as the ID.

Interesting note: on systemd ≥ 250, KDE internally prefers launching apps as transient services (where systemd itself forks the process). But scopes remain the correct mechanism for external launchers that start the process themselves; exactly our case. This is standardized in the systemd XDG desktop specification and isn't going anywhere.

Why is this so hard?

KDE has a clean C++ API for this; KIO::ApplicationLauncherJob is essentially a one-liner that handles scope creation, desktop file lookup, and process management. But no CLI tool exposes it properly:

  • kioclient exec launches .desktop files but doesn't accept app arguments (no --workdir, no directory to open)
  • kstart is a Plasma 5 holdover that doesn't create scopes
  • systemd-run --scope blocks as a lifecycle manager
  • gtk-launch doesn't create KDE-style scopes

There's a missing CLI tool in the KDE ecosystem; something like klaunch org.kde.dolphin /some/path that does what the start menu does. Until that exists, the busctl + StartTransientUnit approach is the way to go.

Environment

OSKubuntu 25.10 (Questing Quokka)
KDE Plasma6.4.5
systemd257
Display serverWayland (KWin)