Skip to content

fix: write EXIF metadata and the sRGB profile into TIFF exports - #1752

Open
FabianWeiss90 wants to merge 8 commits into
CyberTimon:mainfrom
FabianWeiss90:fix/tiff-metadata
Open

FabianWeiss90 wants to merge 8 commits into
CyberTimon:mainfrom
FabianWeiss90:fix/tiff-metadata

Conversation

@FabianWeiss90

@FabianWeiss90 FabianWeiss90 commented Sep 20, 2026 •

Copy link
Copy Markdown

Description

Fixes #1750. Also fixes #1489 for TIFF, with @yojen7's agreement — see below.

TIFF exports carried no metadata, because metadata is patched into the encoded
bytes afterwards with little_exif — which is safe for JPEG/PNG/WebP, but not
for TIFF: its pixel data is addressed through absolute StripOffsets stored in
the IFD, so inserting entries into a finished file moves the pixel data away
from the offsets pointing at it. Hence the FIXME guard that skipped the
format.

This writes the tags while encoding instead, through the tiff crate's
low-level encoder (the same crate image already uses, and the same mechanism
image's own TiffEncoder::set_icc_profile() uses internally). Because every
tag value is known before a single pixel is written, the Exif and GPS sub-IFDs
are written first, their offsets are kept, and the main IFD then points at
them — all before write_data() lays out the strips. Nothing needs patching,
so nothing can be corrupted.

The result is spec-shaped: shooting data in a proper Exif sub-IFD (0x8769),
GPS in its own (0x8825), descriptive tags in IFD0 — not flattened into the
main IFD.

One dependency change worth a look: tiff becomes a direct dependency. It
is already in the binary through image, and is pinned to the same version, so
nothing is added — but build.rs now fails the build if those versions ever
drift apart. Details under Changes Made.

The same fix applies to the outputs that never went through the export path at
all (denoising, negative conversion, panorama stitching, HDR merging, focus
stacking). Those wrote their files with image::save() and no metadata,
leaving the data readable only through RapidRAW's own sidecar. They now embed
exactly what that sidecar already records — sidecar writing itself is
unchanged, so RapidRAW and external tools agree.

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Performance improvement
  • Code refactoring
  • Documentation update
  • UI/UX improvement
  • Build/CI or Dependency update

Changes Made

  • New src-tauri/src/tiff_metadata.rs: encodes a TIFF with its metadata in
    the right IFDs. Supports 8-bit, 16-bit and 32-bit float (panorama and HDR
    keep float data).
  • tiff becomes a direct dependency, pinned to the version image already
    resolves, so no second copy is built. image does not expose its TIFF
    encoder beyond new()/encode()/set_icc_profile(), so writing custom tags
    is not possible through it. build.rs reads Cargo.lock and fails the build
    with an explicit message if the two versions ever drift apart — otherwise
    Cargo would silently resolve two copies of the crate into the binary.
  • Tags are carried over generically, not from a hand-written list: each
    tag's group, number and format are read from little_exif and written with
    the matching TIFF type, so everything the existing gathering stage produces
    survives, including what copy_full_exif_from_source() copies wholesale.
  • Structural tags are filtered out (ImageWidth, StripOffsets,
    BitsPerSample, Compression, RowsPerStrip, tile and thumbnail tags,
    stale sub-IFD pointers) — the encoder writes its own, and copying the
    source's would describe the wrong image. MakerNote goes too, since it holds
    offsets into the source file.
  • The "source is a TIFF" guard is removed. That guard existed because
    copying IFD0 wholesale from a TIFF source drags those layout tags into the
    output. The filter above removes exactly those, so the guard is no longer
    needed — and removing it is what makes a re-export of a denoised or stacked
    TIFF keep its metadata.
  • Metadata gathering is split out into
    exif_processing::collect_metadata_from_source(), so it can run before
    encoding instead of after. JPEG/PNG/WebP behaviour is unchanged.
  • The sRGB ICC profile is tagged in TIFF too (tag 34675, with the
    UNDEFINED type the specification asks for), so TIFF is no longer the one
    export format without it. Export and the conversions share the writer, so
    their TIFFs are covered as well.
  • The two TIFF writers are merged into one. feat(export): add DPI setting for print exports #1910 started encoding TIFF
    through the tiff crate as well, to write the resolution tags for its new
    DPI setting — the same technique this PR uses for EXIF. Rather than leave two
    encoders side by side, encode_tiff_with_metadata now takes the DPI and
    records it as the image resolution, and the write_tiff helper from feat(export): add DPI setting for print exports #1910 is
    gone. Its resolution tags for JPEG and WebP move into write_exif_metadata,
    since TIFF takes its resolution from the encoder and the structural-tag
    filter would drop them from a patched EXIF block anyway.
  • PixelXDimension/PixelYDimension now come from the encoded image rather
    than the source, so a resized export reports its own dimensions. In TIFF they
    are left out entirely, since TIFF records its size in IFD0 and the Exif
    specification does not allow them there — exiftool -validate flags it.

Note on sub-IFD pointers: they are written as LONG, not as the TIFF IFD
type. The latter is legal TIFF but not what the Exif specification asks for,
and readers reject it (kamadak-exif returns InvalidFormat("Invalid pointer")).

On the ICC profile: #1820 tagged JPEG and PNG but had to leave TIFF out,
because this PR replaces the branch that would have applied it — image's
encoder is no longer involved there. Since that profile is now in the repo,
the tag is written here directly instead, which closes #1489 for TIFF too.
@yojen7 and I agreed on this order rather than a follow-up PR.

Screenshots/Videos

Testing

  • These changes were tested locally by a human and confirmed to work.
  • I haven't added any automated tests to the code because the codebase currently lacks a test suite.

Verified manually with a Sony SLT-A77V file: Make, Model, f/4, 1/60 s, ISO 320,
focal length, lens model, capture date, ExifVersion and orientation all present
in the exported TIFF, which decodes at full resolution. Repeated with that TIFF
as the source — the case the removed guard existed for — with correct results
and strip offsets belonging to the new file. Pixel data was checked to survive
unchanged at 8-bit, 16-bit and 32-bit float, and an export with metadata turned
off still produces a file without any.

Checked every output path with exiftool after merging #1897, going through the
real export encoder rather than the TIFF writer alone, with a Canon CR3 as the
source: JPEG, PNG and TIFF exports (8-bit and 16-bit) all carry the sRGB
profile, the camera and lens, a valid capture date and the white balance, as do
the TIFFs the conversions write. Patching EXIF into a finished JPEG or PNG
afterwards leaves the profile intact, the two living in different segments, but
it was worth confirming. An export with metadata switched off still carries the
profile, which seems right: it describes how to interpret the pixels rather than
where the photo came from.

The DPI setting from #1910 was checked through this path as well: at 300 DPI the
TIFF reports XResolution: 300 with ResolutionUnit: inches, and without the
setting it keeps the encoder's default, exactly as before.

exiftool -validate reports Validate: OK on the written TIFFs, with no
warnings at all. That is partly thanks to #1897: it added the ExifVersion and
FlashpixVersion tags the specification requires, and since the merge resolution
puts them in the shared gathering step, TIFF carries them too.

cargo clippy --all-targets -- -D warnings and cargo fmt --check are clean.

Test Configuration:

  • OS: Fedora 44 (Wayland, kernel 7.2.5)
  • Hardware: AMD Ryzen 9 5900X, AMD Radeon RX 7800 XT (Mesa/radeonsi), 32 GB RAM

Checklist

  • My code follows the project's code style
  • I haven't added unnecessary AI-generated code comments
  • My changes generate no new warnings or errors

Additional Notes

Rebased onto current main thrice while this was open, which is worth a note
since both times the same code moved underneath it:

  • fix(export): write complete, valid EXIF for RAW sources #1897 split write_image_with_metadata into write_exif_metadata and
    embed_xmp, while this branch had split the same function differently to
    reach the gathered metadata before encoding. Resolved in favour of the
    upstream structure, with the gathering step extracted back out into
    collect_metadata_from_source(). Its new ExifVersion and FlashpixVersion
    tags live in that shared step now, which is why TIFF validates cleanly.
  • feat(export): add DPI setting for print exports #1910 added the DPI setting and its own TIFF encoder, merged into this
    one as described above.

Four things this PR leaves out on purpose, all easy to add if you would rather
have them here:

  • Panorama, HDR and focus stacking embed the metadata of the first source
    image, because that is what their sidecar already records. Merging metadata
    across the inputs (keeping only values identical in all of them, leaving the
    differing ones empty) would be more correct for exposure time and ISO, but it
    changes what those outputs claim, which is a separate decision.
  • TIFF gets no XMP. fix(export): write complete, valid EXIF for RAW sources #1897 writes keywords, rating and label into JPEG, PNG and
    WebP through format-specific embedders, and there is none for TIFF — it would
    mean writing tag 700 in the new writer. Worth doing, but it is that PR's
    feature rather than this one's.
  • The PNG that denoising, panorama stitching and HDR merging write carries its
    EXIF but no colour profile, since only exports are tagged today. That leaves
    those conversions tagging their TIFF but not their PNG, which is a handful of
    lines through image's PNG encoder.
  • WebP exports remain untagged; that is the webp crate's doing rather than
    this code, and it was already the case before.

AI Disclaimer:

Please state the involvement of AI in this PR:

  • This PR is created by an AI agent
  • This PR is mostly AI-generated but edited/merged together by a human
  • This PR was handwritten with AI assistance (spell check, logic suggestions, error resolving)
  • This PR contains only blood, sweat, and coffee (AI-free)

The TIFF encoder needs to write EXIF tags itself, which image does not
expose. Pin it to the version image resolves and fail the build if the
two ever drift apart into two copies of the crate.
Patching a finished TIFF invalidates the strip offsets stored in its
IFD, so metadata writing was skipped for the format. Write the tags
while encoding instead, into a proper Exif and GPS sub-IFD, before the
strip offsets are laid out.

Copying metadata from a TIFF source now skips the tags describing its
pixel layout, which is what the separate source-TIFF guard protected
against.
Denoising, negative conversion, panorama stitching, HDR merging and
focus stacking wrote their output without any metadata, leaving it
readable only through RapidRAW's own sidecar. Embed what that sidecar
already records, so other tools see it too.

Panorama and HDR keep their 32-bit float data, so the TIFF encoder
learns that sample format here.
exiftool -validate flagged two issues in the written files. ExifImageWidth
and ExifImageHeight are not allowed in TIFF, which states its size in IFD0,
so they are left out. Odd-length text values pushed everything written
after them onto an odd offset, so ASCII values are padded to even length.
@yojen7

yojen7 commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

Heads-up on an overlap: I have a small change ready that embeds an sRGB ICC profile (CC0, 736 bytes) in JPEG/PNG/TIFF exports for #1489, via ImageEncoder::set_icc_profile. It conflicts with this PR only in the TIFF branch of encode_image_to_bytes, since you replace it with encode_tiff_with_metadata. Happy to go either way: I can wait for this to land and then add the ICC tag (34675) to your TIFF writer, or you could take the profile directly. Which would you prefer?

@FabianWeiss90

Copy link
Copy Markdown
Author

Thanks for the heads-up — good to catch that early.

I'd go with your first option: let this land, then add the ICC tag on top. After this PR the TIFF tags all get written in one place, and the tiff crate already has Tag::IccProfile (34675) in its tag enum, so it's a three-liner in write_main_ifd next to the Exif and GPS pointers. The other way round I'd have to pull your profile blob and its licensing into a PR that's about something else, and carry code that isn't upstream yet. Your JPEG/PNG parts don't conflict with this at all, so they can go ahead independently either way.

Heads-up for the follow-up: ImageEncoder::set_icc_profile won't work for TIFF afterwards, since that branch no longer goes through image — writing EXIF during encoding is the whole point. Plain write_tag instead.

If the maintainer would rather take yours first, fine by me — then I'll rebase and add the tag myself, with your profile and credit.

@yojen7

yojen7 commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

Sounds good, thanks. I'll send the JPEG/PNG part on its own now and add the ICC tag in write_main_ifd after this lands.

CyberTimon pushed a commit that referenced this pull request Oct 3, 2026
Exports are sRGB-encoded but carried no color profile, so color-managed
apps and print services had to assume sRGB (#1489). Embed the CC0
sRGB-v2-magic profile (736 bytes, from saucecontrol/Compact-ICC-Profiles)
through image's ImageEncoder::set_icc_profile. Pixel data is unchanged.

TIFF follows once #1752 lands; WebP, AVIF and JPEG XL are unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@yojen7

yojen7 commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

A related EXIF finding you may want for this PR, since it touches the same file: #1161, where the UserComment is written without its 8-byte character-code header. Readers then report "invalid charset" and drop the first 8 characters.

The fix is small (+14/−2 in exif_processing.rs): write the ASCII\0\0\0 header, or UNICODE\0 with UTF-16 for non-ASCII text. Checked with a headless export plus exiftool -v3: the warning goes away and non-ASCII comments read back exactly. It's on yojen7:fix/1161-user-comment-charset, rebased on current main and on different lines from your changes.

Happy for you to fold it into #1752, or I'll open a separate PR after #1752 lands, whichever you prefer. (Found and written by an AI agent, like my other PRs.)

# Conflicts:
#	src-tauri/src/export_processing.rs
#	src-tauri/src/lib.rs
@FabianWeiss90

Copy link
Copy Markdown
Author

@yojen7 your ICC PR landed first, so the order we discussed is reversed — I've merged main into this branch and your profile is in here now.

TIFF is still untagged, and since this PR replaces that branch with its own writer, the tag has to go in there: tiff has Tag::IccProfile built in, so it's a write_tag call next to the Exif and GPS pointers, maybe five lines. Want me to add it here so #1489 is done in one go, or would you rather do it on top once this lands? Happy either way — just say which.

JPEG and PNG exports carry the profile since it was added to the repo,
but TIFF could not: it no longer goes through image's encoder, which is
what applies it. Write the tag directly instead, in the main IFD and with
the UNDEFINED type the specification asks for.

This covers the conversion outputs as well, since they share the writer.
@yojen7

yojen7 commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

Please go ahead and add it here, thanks. One PR for #1489 is simpler. Once it's in, I'll check that exported TIFFs carry the profile and that it matches what the JPEG/PNG exports embed.

@FabianWeiss90 FabianWeiss90 changed the title fix: write EXIF metadata into TIFF exports and conversion outputs fix: write EXIF metadata and the sRGB profile into TIFF exports Oct 5, 2026
@FabianWeiss90

Copy link
Copy Markdown
Author

@yojen7 Done. As soon as this PR lands, it should be in there

CyberTimon#1897 split write_image_with_metadata into write_exif_metadata and
embed_xmp, while this branch had split the same function differently to
get at the gathered metadata before encoding. Resolved in favour of the
upstream structure: the gathering step is extracted out of
write_exif_metadata into collect_metadata_from_source again, which the
TIFF encoder calls before writing any pixels.

ExifVersion and FlashpixVersion, which CyberTimon#1897 adds, sit in that shared
step now, so TIFF carries them as well and exiftool -validate passes
without warnings.
@FabianWeiss90

Copy link
Copy Markdown
Author

Resolved conflicts with current upstream changes.

CyberTimon#1910 added a DPI setting for print exports and, for TIFF, started
encoding through the `tiff` crate to write the resolution tags — the same
technique this branch uses for EXIF. That left two TIFF writers side by
side, so they are merged into one: `encode_tiff_with_metadata` takes the
DPI and records it as the image resolution, and the `write_tiff` helper
added upstream is gone.

The resolution tags for JPEG and WebP move to `write_exif_metadata`,
since TIFF gets its resolution from the encoder rather than from the
patched EXIF block, and the structural-tag filter would drop them anyway.
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.

BUG: TIFF exports and all conversion outputs contain no metadata Feature request: Embed/select ICC color profile on export

2 participants