Skip to content

Fix export filename extension case handling (fixes #19050) - #21863

Open
sjmudd wants to merge 2 commits into
darktable-org:masterfrom
sjmudd:export-extension-case-19050
Open

Fix export filename extension case handling (fixes #19050)#21863
sjmudd wants to merge 2 commits into
darktable-org:masterfrom
sjmudd:export-extension-case-19050

Conversation

@sjmudd

@sjmudd sjmudd commented Aug 15, 2026

Copy link
Copy Markdown

Export filename extension case (fixes #19050)

Problem

Exported filenames always got a hard-coded lowercase extension (.jpg,
.png, ...) appended by the storage module, regardless of what the
filename pattern already produced. Two consequences:

  • No way to control the case of the export extension (e.g. get .JPG
    to match camera-style naming like IMG_1234.CR3).
  • Embedding an extension explicitly in the pattern (e.g.
    $(FILE_NAME).JPG) produced a duplicated extension:
    IMG_1234.JPG.jpg.

There was also no variable exposing the export format's extension.
$(FILE_EXTENSION) reflects the source file's extension only, so
$(FILE_NAME).$(FILE_EXTENSION^^) does not give the export extension
at all — for a CR3 processed to JPEG it expands to IMG_1234.CR3.

Changes

1. Don't double-append a matching extension
(common/utility.c/.h, imageio/storage/{disk,gallery,latex}.c)

Added dt_util_str_ends_with_extension(), a case-insensitive
"does this filename already end in .ext" check. Each storage
module now skips appending its implicit extension when the expanded
pattern already ends with it (in any case). Patterns without an
extension behave exactly as before.

In disk.c, the "generate unique filename on conflict" handling
reuses the same insertion point to write _01, _02, ... suffixes
before the extension on repeated exports to the same target. That
insertion point is now computed consistently whether the extension
was already present in the pattern or just appended, so conflict
suffixes land in the right place (name_01.jpg) instead of after an
already-present extension (name.JPG_01.jpg).

2. New $(EXPORT_EXTENSION) variable
(common/variables.c/.h, gui/gtkentry.c,
imageio/storage/{disk,gallery,latex,piwigo}.c)

Exposes the extension of the export format actually being used
(format->extension(fdata)), as opposed to $(FILE_EXTENSION)
which is the source file's extension. Works with the existing
^/^^/,/,, case operators like any other variable.

Result

$(FILE_NAME).$(EXPORT_EXTENSION^^)

now expands correctly and dynamically for whatever format is being
exported to, e.g. IMG_1234.CR3IMG_1234.JPG when exporting to
JPEG, with no duplicated extension.

Scope

disk (file export) and the GUI's EXPORT_EXTENSION availability
cover the reported case. gallery and latex storage got the same
double-append fix for consistency (same underlying pattern, same
bug). piwigo's pattern-driven path doesn't append an extension
itself, so it only gained $(EXPORT_EXTENSION) support, not the
double-append fix (nothing to fix there). email storage builds its
filename internally, not from a user pattern, so it's unaffected.

Testing

All changed files compile cleanly under the project's CI flags
(gcc-16, -Werror -Wfatal-errors, Ubuntu 26.04 toolchain).
Verified end-to-end with darktable-cli exporting a real Canon CR3
sample:

  • $(FILE_NAME).JPG (JPEG export) → single correctly-cased
    extension, no duplication.
  • Default pattern, no explicit extension (JPEG export) → unchanged
    .jpg behavior, no regression.
  • $(FILE_NAME).$(EXPORT_EXTENSION^^) (JPEG export) →
    IMG_1234.JPG.
  • $(FILE_NAME).$(EXPORT_EXTENSION^^) (PNG export, same source
    file) → IMG_1234.PNG — confirms the variable tracks the actual
    export format rather than being hard-coded to one.
  • $(FILE_NAME).$(EXPORT_EXTENSION,,) (JPEG export) →
    IMG_1234.jpg — lowercase operator works as expected.
  • $(FILE_NAME).JPG (JPEG export) exported three times to the same
    location with "generate unique filename" conflict handling →
    IMG_1234.JPG, IMG_1234_01.jpg, IMG_1234_02.jpg.

Automated tests added:

  • src/tests/variables.c gained a test_export_extension case
    covering $(EXPORT_EXTENSION) with all four case operators, and
    the $(FILE_NAME).$(EXPORT_EXTENSION^^) combination, including a
    case confirming $(FILE_EXTENSION) and $(EXPORT_EXTENSION) stay
    independent (source vs. export format extension).
  • New cmocka unit test src/tests/unittests/util/test_utility.c
    for dt_util_str_ends_with_extension() and the extracted
    dt_util_str_extension_offset() helper, including the exact
    matching-extension-plus-conflict-suffix case that caused the
    disk.c bug above.

Storage modules (disk, gallery, latex) unconditionally appended the
export format's extension after variable expansion, even when the
filename pattern already produced one -- so a pattern like
$(FILE_NAME).JPG became name.JPG.jpg. Now they skip the implicit
append when the expanded name already ends in a matching extension
(case-insensitive).

In disk.c, the "generate unique filename on conflict" handling reuses
the same insertion point to write "_01", "_02" suffixes before the
extension. That insertion point is now computed consistently via
dt_util_str_extension_offset() whether the extension was already
present or just appended, fixing a related duplicate-extension bug in
the conflict path.

Also add $(EXPORT_EXTENSION), exposing the extension of the format
actually being exported to, as opposed to $(FILE_EXTENSION) which is
the source file's extension. This lets $(FILE_NAME).$(EXPORT_EXTENSION^^)
produce the correct extension dynamically, matching camera-style
uppercase naming, for whatever export format is selected.
Comment thread src/tests/unittests/util/CMakeLists.txt Outdated
LINK_LIBRARIES lib_darktable cmocka)

if(WIN32)
target_link_libraries(test_utility PRIVATE lib_darktable)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering why this is needed only for windows, does the add_cmocka_test(... LINK_LIBRARIES lib_darktable) not work properly on windows?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding is Windows has no RPATH, so the DLL must be copied next to the test binary manually or it won't be found at runtime.

@Donatzsky

Donatzsky commented Aug 16, 2026

Copy link
Copy Markdown

What happens in these case?

$(FILE_NAME).JPG (PNG export).
$(FILE_NAME).JPG (PNG export) exported three times.

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.

Make export filename of extensions more flexible

3 participants