Skip to content

feat(tools): Metadata tool - #1108

Open
lightscape-jm wants to merge 3 commits into
KnifeLemon:mainfrom
lightscape-jm:feat/metadata-tool
Open

lightscape-jm wants to merge 3 commits into
KnifeLemon:mainfrom
lightscape-jm:feat/metadata-tool

Conversation

@lightscape-jm

@lightscape-jm lightscape-jm commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

What does this change?

Closes #362. Adds Metadata, a palette tool to look at every metadata field of a picture, PDF, video or sound file and choose what to keep, as agreed in #362 and planned in #856 (step 6, after Add background). It is in the palette's Edit tab, not on a default donut. It sits next to the Remove metadata preset and doesn't replace it: the preset stays the one-click "remove it all", and this is for looking and choosing. The Balanced / Maximum privacy / Custom presets of the request are left for a later PR, as you suggested.

  • Window: the file's fields grouped by where they live (Location first, with a warning that anyone with the file can see where it was made; then Camera, Exposure, Description, Keywords, Document, the rest, File last), and a search box.
    • Per field: edit the value, remove it (×), put it back (the arrow). A list (keywords, creators) is edited one entry per line, so "Smith, John" stays one creator.
    • Add field: title, description, creator, subject and rights (XMP); keywords, object name, caption, by-line and copyright notice where the kind of file has IPTC (JPEG, TIFF, PSD); title, author, subject and keywords for a PDF.
    • Remove location and Remove everything (keeps the orientation and the colour profile, so the picture looks the same; the hint says locked fields may stay). A video keeps its header dates, which ExifTool can't remove (they show a lock).
    • What Remove everything doesn't offer to clean: camera RAW and DNG, as ExifTool warns against it and what it would take is what a raw converter needs; and a file that holds more than its picture (a Motion Photo's video, an MPF second picture, a gain map, a Samsung trailer). That one says so and points to the Remove metadata preset: the second picture or video has metadata of its own that no field shows, and -all= would orphan or lose it. JPEGs from recent cameras and Pixel phones can be in this group. Remove location stays on offer there, and the notice says the embedded metadata can't be reached.
    • What can be edited is a choice, not what's left over: IFDs, PDFs and PNGs offer only the fields that describe the file. The rest (a TIFF's layout, a DNG's colour calibration, a PDF's page mode, a PNG's colour chunks, sizes, versions) shows a lock, as do a video's or HEIC's header and tracks. A kind of file ExifTool doesn't write (MP3, FLAC, WAV, MKV, AVI, BMP) shows read-only and says so; without ExifTool, a kind MetadataExtractor can't read (a PDF) says it needs ExifTool and offers it.
    • Several files one after another, "1 of 3", Skip and Save & next; a summary at the end when a file couldn't be read.
  • Saving: an ordinary conversion in the toast, as for Pixelate / blur, Annotate and Add background: "{name}_metadata" by the tool's output rule, or Replace the original (the original goes to the Recycle Bin / Trash, as the output option does elsewhere). The tick is the tool's own output rule, so it is remembered.

Only the metadata is rewritten, never the picture, sound or document. This was the bar for the tool, so it is built and tested around it:

  • A conversion with changes (Preset.Metadata) has a route of one kind only: ConversionEdge.Metadata, which no ordinary route takes, and which is the whole route when asked for (as Whisper's caption edge is). Without ExifTool the file fails (error.no_route, with the route asked for, so filee can name the package to install); it is never re-encoded by another engine instead. A signed .p7m is refused the same way: opening it for the file inside would lose the changes.
  • Engines/Metadata/ExifToolConverter has ExifTool edit a copy in a work folder beside the output, then moves it into place, so the output rules (Rename / Overwrite / Skip, Replace the original, Keep dates) work as for any conversion, and a long video needs room only on the output's disk. Tests compare the pixels or the streams of the copy with the original's.
  • The copy is read back, not taken on ExifTool's word. A value it can't take (a date, "sideways" for an orientation), a field unknown or with no place in the file (a PDF field set on a JPEG is "copied" without a word, nothing written), text the field can't hold, a field removed that is still there, an editable field Remove location or Remove everything should have taken, and an orientation that should have stayed refuse the copy and leave none (error.metadata_refused, with ExifTool's words). -all= alone leaves a TIFF's IFD fields, whose descriptive ones go by name on every page.
  • Lists are written one entry at a time, never split on a separator; IPTC text outside Latin-1 switches the file's IPTC to UTF-8 and writes its other IPTC text again, so "Zoë" doesn't become "Zo?" and "한국어" doesn't become "???".
  • Remove location and the window go by one rule (ExifTool.IsLocation), and a small -config makes the GPS…Ref fields macOS writes into XMP removable; ExifTool otherwise reads them and won't touch them, so a picture saved by macOS couldn't be cleaned.
  • No value, field name or file name can become an option: ExifTool edits its copy under a plain name (% in a name would be expanded, and -o refuses WebP); edits go through an argument file, one per line, a value of several lines through a file of its own; field names must be "Group:Tag" and nothing else; a file it reads is an argument of its own, or in the UTF-8 argument file on Windows, where Perl's command line is in the ANSI code page.

How it's built (also in docs/ARCHITECTURE.md → Metadata tool):

  • MetadataEdits (fields set, removed, Remove location, Remove everything), read leniently: an entry that isn't one field, or has no value, is left out. MetadataTool.ConversionOf makes the tool into the conversion; History offers no Retry, as for hidden parts, marks and backgrounds.
  • Engines/Metadata/ExifTool reads with -j -G0:1: a list comes as a JSON array, so its entries stay whole, and IPTC's and a PDF's own fields, whose family and group share a name, are read too. Without ExifTool, MetadataReader (MetadataExtractor) shows the fields read-only, an XMP list as one field, and the window offers Settings → Engines; it reads the file again once ExifTool is installed.
  • MetadataViewModel and MetadataWindow follow Add background: the keys (Enter edits a field or saves, Shift+Enter starts a new line, Esc cancels or closes, Ctrl+F searches, the focus back on the row after an edit, a removal or an undo), names a screen reader says for every value and button, a summary when a file can't be read, texts that follow a language change.

Compatibility: the built-in tool-metadata comes with settings schema 69, the next one free (UserDataStore adds it to existing libraries after Add background, in the palette only). I can renumber it if another change takes 69 first. An older Filee doesn't know the tool and opens nothing for it. ExifTool 13.60 is a new optional engine package (Settings → Engines and filee engines): the Windows build, its exiftool(-k).exe renamed so it doesn't wait for a key, or the Perl distribution on macOS and Linux, pinned with the checksums exiftool.org publishes; a copy on the PATH is found too. MetadataExtractor 2.9.3 (Apache-2.0) is a new NuGet dependency; both are in THIRD-PARTY-NOTICES.md.

What Remove location can't reach, and says so: ExifTool can't delete maker-note fields one by one, and a C2PA block, a PNG's or a PDF's own fields and a GeoTIFF's map position stay too. The window names what stays (ExifTool.LocationOutOfReach). Timed GPS in a video's tracks isn't read at all, so every video that can be edited gets a notice that its tracks may hold more. On macOS and Linux an ExifTool script with no Perl now reports that, from the engine's status and in the window, and the window offers no install that wouldn't help.

Known limits: a row ExifTool refuses (a field it reads but won't write, as a custom XMP property) fails that file's save, with the reason in the toast; the file's other edits go with it, as the window has moved on. A value it coerces isn't caught (abc for an ISO). A file ExifTool itself can't read falls back to read-only. A video's or HEIC's header dates stay. PDF changes are an added update, so earlier values stay recoverable inside the file; the window says so.

Type of change

  • Bug fix
  • New feature or improvement
  • New format or engine
  • Translation
  • Documentation
  • Build, CI or dependencies

How was it tested?

  • dotnet format --verify-no-changes
  • CI now runs the writer: the Windows build fetches the pinned ExifTool and sets FILEE_TEST_EXIFTOOL, and a new metadata (linux-x64) job runs the Metadata tests with the pinned Perl distribution and FFmpeg. If FILEE_TEST_EXIFTOOL is set and the file isn't there, the tests fail rather than skip. Windows CI has no FFmpeg, so the video test runs only in the Linux job there; I ran it by hand on Windows and it passes. fetch-engines.ps1 now sends its own User-Agent, as SourceForge answers PowerShell's with an HTML page and the checksum then fails.
  • dotnet test: Core, Cli and Platform pass, and all the Metadata tests in Core, Engines and App pass on macOS (ExifTool 13.60 and 12.58), in an Ubuntu 24.04 container (pinned 13.60, with FFmpeg, as the new job does) and on Windows 11 ARM64 (13.60; the cases that are about Perl skip there, as ExifTool is a program on Windows). On my Mac the Engines and App tests that fail on unmodified main fail the same way (FFmpeg without some encoders, the engine-page renders, the flaky A_lost_animation_frame…). I didn't run the Windows installer (Setup) tests.
  • A sweep of 265 files, most of them built to match what those apps write (iPhone-style HEIC and MOV, DSLR-style JPEGs, Lightroom-style XMP, PNGs written by macOS, TIFF, PDF, Motion Photos, MPF), Remove everything and Remove location on each with ExifTool 13.60 and 12.58, compared on pixels, colour profile, orientation, and fields lost and left.
  • Tried it in the app (macOS, a build of this branch on current main, ExifTool 12.58, dark): put on the Images donut from the palette's Edit tab; a photo with camera, GPS, XMP and IPTC fields opened from the donut; Remove location, a value changed, Save copy: saved as "venice_metadata.jpg", GPS and City gone, the rest as it was, "Kim, Alex" still one creator, the original untouched, pixels the same; search, a list's editor, Esc; Remove everything: only the orientation, the colour profile and EXIF's mandatory resolution fields left, pixels the same, the next copy "(2)". No errors in the log.

New tests:

Test Checks
Core MetadataToolTests (20) schema 69 adds the tool once, after Add background; the conversion carries the edits and the tool's output rule, the tool left as it was; Replace the original kept in the tool; edits read leniently (no option, wildcard or second line gets in); an ordinary route never takes the metadata edge, a conversion with edits takes nothing else; no engine, no write: the route asked for is told; no History retry
Engines MetadataTests (77) ExifTool reads every field by group, lists whole, IPTC's and a PDF's own fields; what the file decides locked, binary and non-text values never editable; a file it can't read a failed read; changes, removals and additions into a copy, the picture the same; lists entry by entry; IPTC in UTF-8 without garbling Latin-1 text; each refusal leaves no copy; a stand-in ExifTool that says "done" and changes nothing, and one that takes too much, both caught reading the copy back; Remove location over every kind of place field, macOS's XMP and video (streams the same); Remove everything on JPEG, PNG, WebP, TIFF (every page, each page's orientation), a CMYK picture with a print profile (colours the same), PDF (page mode left alone) and video; a thumbnail's orientation never becoming the picture's; PNG gamma kept; comments; %, non-Latin and line-break file names, both ways of passing the path; what's after the picture left alone; the converter's formats, its route, and a conversion end to end (Overwrite, refused); what Remove location can't reach, a place and not a focus point; Remove everything with new text outside Latin-1 over UTF-8 IPTC; what needs Perl and what doesn't. OwnCopyTests checks the engine's status says so
App MetadataWindowTests (69) groups, search, locks; Save sends exactly what changed, lists whole, a value emptied as a removal; Save there while the first change is typed, a click on it saves it; added fields, also while searching; Remove location and Remove everything, undo and remove again; Replace the original kept on disk; read-only without ExifTool and for kinds it doesn't write, by the file's own extension; RAW and files that hold more; Add field per kind of file; a file that can't be read, the summary, a save that can't start; a shared thumbnail left whole; a second open replacing the first window; screen-reader names; the keys and where the focus goes; texts after a language change; dates and coordinates in their own order right to left; every text whole in all 20 languages; the notices of what stays, shown and following the language, on video, not when read-only; no Perl, and no install offer that wouldn't help

Screenshots (UI changes)

A real photo with Remove location pressed and a value changed:

The Metadata window with Remove location pressed and a value changed

Search, and a list edited one entry per line:

The Metadata window searching, with a list edited one entry per line

Checklist

  • New UI strings are in all src/Filee.App/Assets/i18n/*.json files (48 strings, translated in every language)
  • New third-party code or binaries are listed in THIRD-PARTY-NOTICES.md (MetadataExtractor, ExifTool)
  • The PR does one thing (unrelated changes go in a separate PR)

🤖 Generated with Claude Code

A palette tool that shows every metadata field of a picture, PDF, video
or sound file, grouped by where it lives, and lets you edit, remove or
add them, or clear the location or everything at once. Saving is an
ordinary conversion carrying the changes, written by ExifTool through a
route edge of its own, so the file is never re-encoded and the copy is
read back before it counts. ExifTool 13.60 is a new optional engine;
without it MetadataExtractor shows the fields read-only (KnifeLemon#362).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added the feature New features label Nov 4, 2026
Main took settings schema 68 for signed .p7m files, so the Metadata tool moves to 69. An envelope is planned without
PlanThrough when metadata changes are asked for: opening it would drop them without a word, so the file is refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@KnifeLemon KnifeLemon left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Thanks, @lightscape-jm. This is careful work. The argument file and the strict field-name regex leave no injection route that I could find, the original only goes through the existing Replace flow, metadata edges stay out of ordinary routes, v69 sits cleanly on main's 68, and all 44 keys are in all 20 languages.

Two things hold it back. I read the code and didn't run ExifTool, so please correct me if you see something different.

Must fix

1. Remove location can report success while location stays in the file

ExifTool.cs:301 and the read-back at :424-425 only look at f.Editable fields, and EditableFamilies (:28) leaves out maker notes. -ee isn't used either. So:

  • Maker notes: location fields such as Panasonic's City, State and Country stay in the copy, and the read-back doesn't notice them.
  • Video: timed GPS in tracks (GoPro, dashcams, drones) isn't read at all, so nothing says it's still there.

HoldsMore doesn't cover these, and no notice appears. For someone removing location before sharing a file, that success is wrong. At the least, show a notice when fields.Any(f => !f.Editable && IsLocation(f)) or when the file is a video. That means a new key in all the languages. If you can actually remove them, even better.

2. CI never runs the writer

MetadataTests skips without ExifTool ("No ExifTool on this computer"), and neither ci.yml nor portable.yml provides one, so the green checks here say nothing about writing, removal or read-back. Please fetch the pinned ExifTool in CI and set FILEE_TEST_EXIFTOOL, at least on Windows and one Linux job. This feature changes people's files, and later changes need that protection too.

Smaller

  • ExifTool.cs:404: Remove everything plus a new non-Latin-1 IPTC field fails when the file's IPTC is already UTF-8. The early return [] skips writing the character set, -all= wipes it, the text becomes ???, and the read-back refuses the copy. It fails safely, but the save is lost. if (!edits.RemoveAll && fields.Any(...)) fixes it.
  • MetadataViewModel.cs:173,243: on Linux or macOS without Perl, the engine shows as available because its status never checks for Perl. The start then fails and the window says Filee can't change this kind of file, which sends people looking in the wrong place.
  • BuiltInData.cs:334,338, Preset.cs:293: comments say "settings schema 68". They should say 69, since 68 is .p7m.

Everything else looks ready. Once these are in and CI runs the writer tests, this should be quick to merge.

… and answer the review

Remove location could report success with a place still in the file. Maker notes, a C2PA block, a PNG's or
PDF's own field and a GeoTIFF's map position are out of ExifTool's reach, and timed GPS in a video's tracks is
never read. The window now names what stays and warns on every video, in all 20 languages.

CI never ran the writer, as it skipped without ExifTool. The Windows build now fetches the pinned ExifTool, a new
linux-x64 job runs the Metadata tests against the pinned tarball, and a set FILEE_TEST_EXIFTOOL that points at
nothing fails rather than skips. fetch-engines.ps1 sends its own User-Agent: SourceForge answers PowerShell's with
an HTML page, so the checksum failed.

Remove everything keeps the IPTC character set, as -all= wipes it and new text outside Latin-1 turned to ???. On
macOS and Linux an ExifTool script with no Perl now says so, from the engine's status and in the window, instead of
that the file can't be changed. Settings schema comments say 69.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@lightscape-jm

Copy link
Copy Markdown
Contributor Author

Thanks, @KnifeLemon. All of this is in 941f311.

Must fix

  1. Remove location: a notice, as actual removal isn't possible. ExifTool can't delete maker-note fields one by one. The window now names what stays: maker notes (Panasonic's City, for one), a C2PA block, a PNG's or PDF's own fields, and a GeoTIFF's map position, which I found while testing this. Timed GPS isn't read, so every editable video gets a notice that its tracks may hold more. I checked the tag rule against all 21,532 names in exiftool -listx, so a camera's focus point or a schema location doesn't set it off.
  2. CI: the Windows build fetches the pinned ExifTool, and a new metadata (linux-x64) job runs the Metadata tests with the pinned Perl distribution and FFmpeg. A set FILEE_TEST_EXIFTOOL pointing at nothing now fails rather than skips. Getting this working also showed that fetch-engines.ps1 would fail on ExifTool, as SourceForge answers PowerShell's User-Agent with an HTML page, so it now sends its own. Windows CI has no FFmpeg, so the video test runs only in the Linux job; I ran it on Windows by hand and it passes.

Smaller

  • ExifTool.cs:404: fixed as you suggested, with a test that fails without it.
  • No Perl: the engine's status and the window now both say Perl is missing, and the window offers no install that wouldn't help.
  • Schema comments: all three say 69.

Every new test was checked to fail with its fix removed. The strings are now 48, still in all 20 languages, and the description is updated.

One check is red: portable (macos-15, osx-arm64) failed MemoryTrimmerTests.Trimming_decommits_the_managed_heap_that_was_freed. That test comes from main and this PR doesn't touch that code. I can't rerun the job from here, so could you rerun it?

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

Labels

feature New features

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature Request]: Add Metadata Remover for Images and PDFs

2 participants