Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 41 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -154,18 +154,21 @@ jobs:
path: build/.cache/zig-x86_64-windows-0.16.0
key: zig-0.16.0-x86_64-windows

# rhwp, Pandoc and 7-Zip are small; with them the HWP/HWPX and archive engine tests run too (LibreOffice is
# skipped to save time).
- name: Fetch rhwp, Pandoc and 7-Zip for engine tests
# rhwp, Pandoc, 7-Zip and ExifTool are small; with them the HWP/HWPX, archive and metadata engine tests run too
# (LibreOffice is skipped to save time).
- name: Fetch rhwp, Pandoc, 7-Zip and ExifTool for engine tests
shell: pwsh
run: ./build/fetch-engines.ps1 -Only rhwp,pandoc,7zip
run: ./build/fetch-engines.ps1 -Only rhwp,pandoc,7zip,exiftool

# The UI tests on their own first, as the release runs them (there they crashed with 0xC0000005): a crash
# names the test that was running and leaves a dump in TestResults.
- name: Test UI on its own
run: dotnet test tests/Filee.App.Tests --no-build -c Release --blame-crash --blame-hang-timeout 5m --logger "console;verbosity=normal" --results-directory TestResults/app

# FILEE_TEST_EXIFTOOL: the metadata tests write real files, and fail rather than skip if it isn't there.
- name: Test
env:
FILEE_TEST_EXIFTOOL: ${{ github.workspace }}\engines\exiftool\exiftool.exe
run: dotnet test Filee.slnx --no-build -c Release --filter "FullyQualifiedName!~Filee.App.Tests" --logger "trx" --results-directory TestResults

# Builds Setup (src/Filee.Setup) around a stub app and runs it in a sandbox: install, update and uninstall into a
Expand All @@ -192,6 +195,40 @@ jobs:
tests/Filee.Setup.Tests/bin/Release/net10.0/screenshots/
if-no-files-found: ignore

# The metadata writer on Linux, with the Perl ExifTool pinned in engines.json (the build above is its Windows side).
metadata-linux:
name: metadata (linux-x64)
needs: changes
if: ${{ !cancelled() && needs.changes.outputs.code != 'false' }}
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- uses: actions/checkout@v7

- uses: actions/setup-dotnet@v6
with:
global-json-file: global.json

- name: FFmpeg for the video test
run: |
sudo apt-get update
sudo apt-get install -y ffmpeg

- name: Fetch ExifTool
run: |
set -euo pipefail
read -r url sha256 < <(jq -r '.platforms."linux-x64".components.exiftool | "\(.url) \(.sha256)"' src/Filee.Engines/Infrastructure/engines.json)
curl -fsSL --retry 3 -o "$RUNNER_TEMP/exiftool.tar.gz" "$url"
echo "$sha256 $RUNNER_TEMP/exiftool.tar.gz" | sha256sum --check
mkdir "$RUNNER_TEMP/exiftool"
tar -xzf "$RUNNER_TEMP/exiftool.tar.gz" -C "$RUNNER_TEMP/exiftool" --strip-components=1
echo "FILEE_TEST_EXIFTOOL=$RUNNER_TEMP/exiftool/exiftool" >> "$GITHUB_ENV"
# On the PATH too: the conversion test goes through Filee's own search and the Perl check.
echo "$RUNNER_TEMP/exiftool" >> "$GITHUB_PATH"

- name: Test
run: dotnet test tests/Filee.Engines.Tests -c Release --filter "FullyQualifiedName~MetadataTests"

# Windows on ARM64 (#236), built and tested natively: the Explorer menu DLL, Setup, 7-Zip, rhwp and Unhwp's library
# are ARM64 builds, the downloaded engines (Pandoc here) x64 ones under emulation. rhwp and Unhwp come from pinned
# builds of .github/workflows/windows-arm64-native.yml (build/fetch-engines.ps1). Until both are pinned, their tests
Expand Down
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
<PackageVersion Include="PDFtoImage" Version="5.2.1" />
<PackageVersion Include="Unhwp" Version="0.13.0" />
<PackageVersion Include="Markdig" Version="1.5.1" />
<PackageVersion Include="MetadataExtractor" Version="2.9.3" />
<PackageVersion Include="ExcelNumberFormat" Version="1.1.0" />
<!-- 5.2+ requires SkiaSharp 4 (see PDFtoImage above); 5.1.1 uses SkiaSharp 3.119 and HarfBuzzSharp 8.3 like Avalonia. -->
<PackageVersion Include="Svg.Skia" Version="5.1.1" />
Expand Down
2 changes: 2 additions & 0 deletions THIRD-PARTY-NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ Each component remains under its own license.
| SharpCompress (zstd decompression for engine downloads; RAR, 7z and TAR comics) | MIT | https://github.com/adamhathcock/sharpcompress |
| Unhwp | MIT | https://github.com/iyulab/unhwp |
| Markdig | BSD-2-Clause | https://github.com/xoofx/markdig |
| MetadataExtractor (reading metadata for the Metadata tool when ExifTool is not installed) | Apache-2.0 | https://github.com/drewnoakes/metadata-extractor-dotnet |
| ExifTool (optional engine, downloaded on request: the Metadata tool reads and writes with it) | Perl's licence (Artistic or GPL), separate program | https://exiftool.org |
| ExcelNumberFormat | MIT | https://github.com/andersnm/ExcelNumberFormat |
| ExcelDataReader | MIT | https://github.com/ExcelDataReader/ExcelDataReader |
| PdfPig (PDF text, layout and pictures) | Apache-2.0 | https://github.com/UglyToad/PdfPig |
Expand Down
12 changes: 11 additions & 1 deletion build/fetch-engines.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
install: ebook-convert for LIT, LRF, PDB, ... and MOBI / AZW3 output
tesseract/ Tesseract OCR (Apache-2.0, separate program), UB Mannheim's Windows build unpacked from its
installer with 7-Zip (never run); the language data is downloaded by the app when needed
exiftool/ ExifTool (Perl's licence, separate program), the Windows build: exiftool(-k).exe renamed
exiftool.exe (the "-k" makes it wait for a key), with its exiftool_files/ beside it

Every download is pinned to a version and verified with SHA-256 (src/Filee.Engines/Infrastructure/engines.json,
shared with the app). Downloads are cached in build/.cache.
Expand Down Expand Up @@ -144,7 +146,7 @@ function Get-Engine([string]$name, [switch]$X64) {
return $file
}
Write-Host "get $name $($info.Url)"
Invoke-WebRequest -Uri $info.Url -OutFile $file -UseBasicParsing
Invoke-WebRequest -Uri $info.Url -OutFile $file -UseBasicParsing -UserAgent 'Filee-build'
$hash = (Get-FileHash $file -Algorithm SHA256).Hash
if ($hash -ne $info.Sha256) {
Remove-Item $file
Expand Down Expand Up @@ -370,6 +372,14 @@ if ('tesseract' -in $selected) {
if (-not (Test-Path (Join-Path $target 'tesseract.exe'))) { throw 'tesseract.exe not found in the installer.' }
}

if ('exiftool' -in $selected) {
# As EngineInstaller does: the one top folder unpacked, the program renamed so it doesn't wait for a key.
$target = Join-Path $Destination 'exiftool'
Expand-Flat (Get-Engine 'exiftool') $target
Move-Item -LiteralPath (Join-Path $target 'exiftool(-k).exe') -Destination (Join-Path $target 'exiftool.exe') -Force
if (-not (Test-Path (Join-Path $target 'exiftool.exe'))) { throw 'exiftool.exe not found in the archive.' }
}

if ('calibre' -in $selected) {
$msi = Get-Engine 'calibre'
$tmp = Join-Path $cache 'calibre-admin'
Expand Down
51 changes: 51 additions & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,57 @@ the picture (padding of its longer side, corners of its shorter side), so a look
isn't told apart, and the picture itself edited meanwhile isn't looked for (as in Pixelate / blur and Annotate). One
look for all pictures or one each; failures end on a summary, as in Pixelate / blur.

## Metadata tool

"Metadata" (`tool-metadata`, palette only, settings schema 69) shows every metadata field of pictures, PDFs, video and
sound, grouped by where it lives (ExifTool's family-1 groups: GPS, IFD0, ExifIFD, XMP-dc, IPTC, PDF …), and edits them
without touching the file's content.

- Saving is an ordinary conversion, as for Pixelate / blur, Annotate and Add background: `MetadataTool.ConversionOf`
makes the tool into a conversion to the file's own format carrying the changes (`Preset.Metadata`,
`MetadataEdits`), so the toast, History, cancelling and the output rules (where the copy goes, its name
`{name}_metadata`, a name already taken, "Replace the original" sending the original to the Recycle Bin / Trash)
work as for any other file. "Replace the original" is the tool's own output rule, kept on disk. History offers no
Retry for it: a Filee from before it would re-encode the file without the changes.
- Only an engine that writes metadata and nothing else takes such a conversion: `ConversionEdge.Metadata`, never part
of an ordinary route, and with `RoutePlanner.Plan(metadata: true)` the whole route or none. Without one the file
fails (`error.no_route`, with the route asked for, so `filee` can name the package), never re-encoded instead.
- Remove location takes what ExifTool can write. What it can't reach is said, not hidden: a place kept whole with a
block (a camera's maker notes, a C2PA manifest; `ExifTool.LocationOutOfReach`) is named in the window, and a video
always says its tracks can hold location (an action camera's or a drone's route), which ExifTool can't remove.
- `Engines/Metadata/ExifToolConverter` is that engine (`exiftool` package: Windows build renamed from
`exiftool(-k).exe`, the Perl version elsewhere, or one on the PATH). Its edges are the formats `exiftool -listwf`
names; a file of an extension it doesn't write shows read-only, saying so. `ExifTool` reads with `-j -G0:1` (lists as
JSON arrays, so an entry keeps its commas; "Group:Tag" where family and group share a name, as IPTC and PDF do).
- It writes by editing a copy in a work folder beside the output, on its disk, under a plain name, so nothing in the
file's name reaches ExifTool (`%` is expanded in paths it takes, Perl on Windows reads its command line in the ANSI
code page, and `-o` refuses WebP). A `-config` of its own makes the `GPS…Ref` fields macOS puts in XMP writable, so
they can be removed. Edits go through an argument file, one argument per line taken literally, a value of several
lines through `-Tag<=file`, a list one assignment per entry. IPTC text outside Latin-1 switches the file's IPTC to
UTF-8 with its other IPTC text written again, so none is garbled.
- What can be edited is a choice, not what's left over: IFDs, PDFs and PNGs offer only the fields that describe the file
(`IfdFields`, `PdfFields`, `PngFields`). The rest says how to read it (a TIFF's layout, a DNG's colour calibration, a
PDF's page mode, a PNG's colours), and a video's or HEIC's header and tracks can't be changed. Those show a lock.
- `-m` lets a file with a small fault of its own be written. The copy is then read back: a warning naming a changed
field, a field unknown or not writable, text that doesn't fit its character set, a field set that isn't there, one
removed that still is, or an editable field Remove location or Remove everything should have taken refuses the copy
(`error.metadata_refused`) and leaves none. Remove location takes GPS and the XMP, IPTC and QuickTime place fields, and
every editable field `ExifTool.IsLocation` finds, the one rule the window strikes through by; a locked one (a camera's
focus location, in its maker notes) stays, in plain sight. Remove everything is `-all=` keeping the orientation and the
colour profile, a TIFF's descriptive IFD fields by name on every page. It may also take locked rows (maker notes, cover
art, a JPEG's DPI, a PNG's custom text); others stay, a video's header dates and a TIFF's other tags among them, and the
hint says so. A file that holds more than its picture (`ExifTool.HoldsMore`: a Motion Photo's video, an MPF second
picture, a gain map, a Samsung trailer) doesn't offer it, and the window says why: it would orphan what's there and
leave that metadata, which no field shows. Camera RAW and DNG don't offer it either.
- Without ExifTool, `Engines/Metadata/MetadataReader` (MetadataExtractor) lists the fields read-only, and the window
offers Settings → Engines; it reads the file again with ExifTool once it is installed.
- `MetadataViewModel` keeps one row per field (edited, removed, added; a list one entry per line) and turns them into
`MetadataEdits`. Add field offers XMP's descriptive fields, IPTC's where the kind of file has IPTC (JPEG, TIFF, PSD),
a PDF's document info for a PDF. The keys, the summary of files that could not be opened or saved, and texts that
follow a language change are as in Add background.
- PDFs: ExifTool appends its changes as an update, so earlier values stay recoverable inside the file; the window says
so.

## Crop overlay and PDF page editor

The image Crop tool (`tool-crop`) and PDF Crop (`tool-pdf-crop`) open `Views/CropOverlayWindow` instead of running a job:
Expand Down
4 changes: 4 additions & 0 deletions docs/ENGINES.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ Filee chooses engines automatically. Settings → *Engines* shows their status,
| FFmpeg | **optional download** (~100 MB, 272 MB on disk) | **all video and audio**: video ↔ video (H.264, HEVC, AV1), video → animated GIF / APNG, a still frame or every frame, audio and subtitle extraction, audio ↔ audio (also APE, WavPack, TTA, DSD, DTS, ALAC, …), GIF, APNG and photos → MP4/WEBM/MOV, Live / Motion Photos → their video, a clip of a video → an iPhone Live Photo or Android Motion Photo (experimental), images → HEIC, clips or audio files joined into one, pictures → a slideshow MP4, copies without metadata (see "Media tools") |
| Calibre | **optional download** (~216 MB, 660 MB on disk) | rare e-book formats: LIT, LRF, CHM, PDB, PML, RB, SNB, TCR, OEB → EPUB; EPUB → MOBI, AZW3, LIT, LRF, PDB, PML, RB, SNB, TCR; e-books → PDF (ranked above the HWPX writer, see below) |
| Whisper | **optional download** (Windows only; ~157 MB with model, ~160 MB on disk) | **Speech-to-text transcription**: video and audio → SRT subtitles in ~100 languages using OpenAI Whisper (whisper.cpp); runs locally and offline; needs FFmpeg |
| ExifTool | **optional download** (~11 MB on Windows, ~8 MB elsewhere; 30 MB on disk) | **the Metadata tool**: reads every metadata field of pictures, PDFs, video and sound, and writes changes into a copy in the file's own format without re-encoding it (JPG, PNG, TIFF, WEBP, HEIC, AVIF, JPEG XL, GIF, PSD, camera RAW, PDF, MP4, MOV, M4A, 3GP, …; not MP3, FLAC, WAV, MKV, AVI or BMP, which it shows read-only). Without it the Metadata tool shows the fields read-only (MetadataExtractor, built in) |

DOCX, XLSX, XLS, ODS and PPTX → PDF need neither Microsoft Office nor LibreOffice: they are read in-process, written as HWPX
and rendered by rhwp. Anything the built-in readers understand (including PDF) is written as DOCX by the DOCX writer.
Expand Down Expand Up @@ -110,6 +111,9 @@ they can be installed or removed later in Settings → *Engines*. A conversion t
installer files and bundled language data are removed (`TrimTesseract`, ~80 MB). The languages come from
`tessdata_fast` 4.1.0, pinned like the engines in `Infrastructure/ocr-languages.json` (`traineddata` kind, one
folder per language under `engines/ocr-languages`), and are downloaded the first time a preset uses one.
- ExifTool is Phil Harvey's Windows build (a Perl bundled with it), whose `exiftool(-k).exe` is renamed `exiftool.exe`:
the "-k" makes it wait for a key before it exits. On macOS and Linux it is the Perl distribution
(`Image-ExifTool-*.tar.gz`), run by the Perl both come with.

### Microsoft C++ runtime (Windows)

Expand Down
Loading
Loading