Skip to content

feat: choose the compression level, with the estimated size (#1353) - #1386

Merged
KnifeLemon merged 10 commits into
KnifeLemon:mainfrom
Ugoyo:claude/compression-level-1353
Nov 12, 2026
Merged

KnifeLemon merged 10 commits into
KnifeLemon:mainfrom
Ugoyo:claude/compression-level-1353

Conversation

@Ugoyo

@Ugoyo Ugoyo commented Nov 11, 2026

Copy link
Copy Markdown

What does this change?

Closes #1353. The four "Compress" tools can ask for the level each time. Dropping files on one opens a small "Choose the compression" window, built like "Choose pages" (#981). It shows the preset editor's own setting for each kind of file dropped (picture quality slider, video quality, audio bitrate, PDF level) and the size the files will roughly come to: "About 30.0 MB (70% smaller)". The window says it is an estimate. Enter compresses at that setting, which the preset keeps for next time. Esc starts nothing.

  • Preset.AskCompression is off by default, so there is no settings migration (same approach as AskPageRange). It is on for compress-image, compress-video, compress-audio and compress-pdf in a new install only. Existing users turn it on in the preset editor: "Ask every time" sits under the setting it asks for, with a hint, and is not offered for merges. Watch folders and the CLI use the preset's own values.
  • Files with nothing to choose (a PNG on "Compress image", a document) convert without the window. Merges keep the preset's values.
  • It reuses what exists: the editor's controls and strings, MediaQuality, PdfCompression, AudioBitrateKbps. The image MaxSizeMb and video MaxSizeMb / MaxSizePercent limits cap the estimate, as they cap the result. No new compression settings.

How the size is estimated (Filee.Engines/CompressionEstimator.cs):

Kind Estimate How close
Audio encoder bitrate × length (ffprobe) within a few % in the tests
Video the bitrate Filee already uses to give a GPU encoder the same quality as x264's CRF (HardwareVideo.BitrateKbps) + audio, × length. A video that would be copied keeps its size. rough: depends on the picture, and the window says so
Picture a full-resolution tile (≤ 640×640) from the middle is really encoded at the chosen quality, then scaled to the whole picture's pixels. Encoding all of it on every slider step is too slow, and a downscaled copy overestimates. within 25 % of the real output in the tests
PDF the built-in PdfCompressor really runs into a byte-counting stream, once per level ≈ exact for the PDFsharp route (Ghostscript can only make it smaller)

"Only keep if smaller" and the size limits cap the estimate. Only the first files of each kind are read (12 pictures, 4 PDFs, 40 media files); the rest count at the same ratio.

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 Filee.slnx --verify-no-changes --severity warn, and dotnet build Filee.slnx -c Release (0 errors)
  • dotnet test, in a Linux container:
    • New: AskedCompressionTests (Core), CompressionEstimatorTests (Engines), AskCompressionTests (App). They hold the estimate against real conversions: picture, PDF, audio and video. The audio and video tests ran with the distro's FFmpeg. The App test drives the real donut → window → Enter/Esc → compressed file.
    • Core: all pass except FolderCopyTests.Original_that_cannot_be_deleted…, which also fails on main here (the container runs as root).
    • App: 2280 passed, 8 failed out of 2335. Seven of the failures also fail on main in this container (glyph tests for CJK, emoji and Bengali, the Engines page). The eighth, RenderTests.Reset_to_default_sits_beside_delete… (ar), passes when run on its own: it is flaky in the full run, like AskPagesTests (ja, dark) on main.
    • Engines: the new tests pass. The full Engines suite was not run again: on main it already has 18 failures here, because Pandoc and LibreOffice are missing.
  • Tried it in the app: not done. Only headless rendering was checked, no real desktop session and no Windows or macOS.

Screenshots (UI changes)

The window, from the headless render tests. The sizes come from a fixed fake estimate (100 MB → 30 MB) used only for the render; the full Windows path is how the test feeds a file name on Linux.

Picture (light) Video (dark, French) PDF (French)
ask-compression-en-light-image ask-compression-fr-dark-video ask-compression-fr-pdf

"Ask every time" in the preset editor, under the setting it asks for:

Compress image Compress video (dark, French)
ask-compression-editor-compress-image-en-light ask-compression-editor-compress-video-fr-dark

Checklist

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

)

Preset.AskCompression, off by default: no settings migration. On for the
four "Compress" presets in a new install only, as Split PDF's pages (KnifeLemon#981).

AskedCompression decides which drops ask (not a merge, group or tool) and
what each file gets: the quality of a picture in a lossy format, the
quality of a video, the bitrate of sound, the level of a PDF. A PNG or a
document converts as the preset says. Apply gives the preset as it runs,
and the arithmetic the window's estimate uses (bitrate x length, the size
limit and "only keep if smaller" as caps).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BPxh5siZR4PzK81twSxnAb
…emon#1353)

CompressionEstimator reads the dropped files once and estimates their
total at each setting the window offers:

- video and audio: the bitrate the encoder is given (for a constant
  quality video, the table Filee already uses to give a graphics chip the
  same quality) times the length ffprobe reads; a copied video keeps its
  size;
- pictures: a full-resolution tile from the middle is really encoded at
  the quality and scaled up to the whole picture;
- PDF: the built-in compressor really runs, into a stream that only
  counts bytes, once per level.

The preset's size limit and "only keep if smaller" cap the result. Only
the first files of each kind are read; the others count at their ratio.
Tests hold the estimate against real conversions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BPxh5siZR4PzK81twSxnAb
…eLemon#1353)

Files dropped on a preset that asks for its compression open a small
"Choose the compression" window first, like "Choose pages" (KnifeLemon#981): the
preset editor's own setting for each kind of file dropped (picture
quality, video quality, audio bitrate, PDF level) and about what the
files come to ("About 30.0 MB (70% smaller)"), estimated again a moment
after each change and said to be an estimate. Enter compresses at that
setting, which the preset keeps for the next time; Esc starts nothing.
Files with nothing to choose (a PNG) convert without the window.

- Preset editor: "Ask every time" under the setting it asks for, with a
  hint; not for a merge.
- Strings in all 22 languages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BPxh5siZR4PzK81twSxnAb
…nifeLemon#1353)

The window disposes the estimator when it closes; Dispose now waits for an
estimate under way instead of pulling its pictures and lock from under it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BPxh5siZR4PzK81twSxnAb
@Ugoyo
Ugoyo requested a review from KnifeLemon as a code owner November 11, 2026 12:04
@github-actions github-actions Bot added the feature New features label Nov 11, 2026

@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.

Thank you, this is a careful take on #1353. It reuses the editor's own settings, follows "Choose pages" closely, and the estimate is checked against real conversions. The screenshots helped a lot.

One thing to fix before we merge:

  1. Files that can't be estimated drop out of the total. In CompressionEstimator.Estimate, a sampled file whose EstimateOne returns null is skipped. It isn't added to sourceKnown, and it isn't scaled at the ratio like the unsampled files, so its bytes disappear from the total. The window's "size now" still counts them, so the estimate reads too small and "% smaller" too large.
    The common case is HEIC. LossyImageTargets includes heic, but ImageMagick can't encode HEIC here (Filee writes it through FFmpeg), so ImageBytes throws on ToByteArray(MagickFormat.Heic) and returns null. A JPG + HEIC drop from a phone leaves the HEIC out of the estimate, and HEIC alone shows "can't estimate". A video file without a video stream on "Compress video" behaves the same way.
    Could you count such files at their kind's ratio, as the unsampled ones are? For HEIC, either estimate it another way or leave it out of KindOf until it can be estimated. A test with one file that can't be estimated next to one that can would pin it down.

  2. Keep the choice to this run. Right now a level picked in the window is saved into the preset, and watch folders and the command line use that preset too, so a one-off choice on the donut quietly changes them. Please apply it to this conversion only, the way Choose pages does. If you'd like a way to keep it, a Remember for this preset checkbox (off by default) is fine.

We'll start CI: first-time PRs need a maintainer to approve the workflow run.

…KnifeLemon#1353)

Review of KnifeLemon#1386: a read file whose estimate failed dropped out of the
total while the window's "size now" still counted it, so the estimate
read too small and "% smaller" too large. Such files now count at their
kind's ratio, as the files that weren't read do; a kind with no file
estimated still gives no estimate.

HEIC leaves the window: Filee writes it through FFmpeg and ImageMagick
can't encode the tile the estimate rests on. A HEIC file converts with
the preset's own quality, as a PNG does, and AskedCompression.Runs keeps
a level chosen for the JPGs from reaching the files without one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BPxh5siZR4PzK81twSxnAb
…1353)

Review of KnifeLemon#1386: the level picked in the window was saved into the
preset, which watch folders and the command line also use, so a one-off
choice on the donut quietly changed them. It now applies to this
conversion only, as "Choose pages" does; a "Remember for this preset"
checkbox, off by default, keeps it. Files without a level convert in a
run of their own with the preset as it is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BPxh5siZR4PzK81twSxnAb
@Ugoyo

Ugoyo commented Nov 11, 2026

Copy link
Copy Markdown
Author

Thanks for the review! Both points are addressed:

  • Files that can’t be estimated now count at their kind’s ratio instead of dropping out of the total (new test: a damaged JPG next to a good one). HEIC is left out of KindOf until it can be estimated; such files (and PNGs) convert in their own run with the preset as it is, so a level chosen for the JPGs never reaches them.
  • The choice now applies to this conversion only. A “Remember for this preset” checkbox (off by default) keeps it.

Resolves the conflict in PresetEditorView.axaml: the iPhone ringtone hint
(KnifeLemon#1349) and the compression "Ask every time" checkbox both go under the
media options.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BPxh5siZR4PzK81twSxnAb
@Ugoyo
Ugoyo requested a review from KnifeLemon November 11, 2026 18:20

@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 for the quick turnaround, both points are done well: the files that can't be estimated now count at their kind's ratio, and the choice stays with this conversion unless "Remember for this preset" is ticked.

Two small things came with splitting the drop into two runs:

  1. "Ask every time" asks twice. When the preset asks where to save, a mixed drop (say JPGs and a PNG on "Compress image") now calls AskThenConvertAsync once per run, so the save dialog opens twice for one drop and the two parts can end up in different folders. Could you ask for the location once and use it for both runs (or keep them in one job with the setting per file)? Without "Ask every time", two jobs are fine.
  2. The checkbox still shows for HEIC. ShowAskCompressionImage uses the editor's LossyTargets, which still has heic, but AskedCompression.KindOf no longer does. So a custom "→ HEIC" preset offers "Ask every time" for the compression, saves it, and then never opens the window. Could you show the checkbox only for "same as source" and the targets in AskedCompression.LossyImageTargets? The quality slider can stay for HEIC.

Main has moved on since: #1370 and #1389 touched Preset.cs and PresetEditorViewModel.cs, so the branch conflicts now. You don't need to resolve that. We'll merge main when we merge. If you'd rather do it with these fixes, that's fine too.

…et (KnifeLemon#1353)

Review of KnifeLemon#1386:

- "Ask every time" asked twice: with a mixed drop (JPGs and a PNG on
  "Compress image") each run opened its own save dialog, and the two parts
  could land in different folders. The place is now asked once for the
  whole drop and goes to every run (AskedCompression.Runs takes it).
- The editor offered "Ask every time" for a HEIC target, which never opens
  the window. The checkbox now shows only for "same as source" and the
  targets in AskedCompression.LossyImageTargets; HEIC keeps its slider.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BPxh5siZR4PzK81twSxnAb
Resolves the conflicts with KnifeLemon#1370 and KnifeLemon#1389 in Preset.cs and
PresetEditorViewModel.cs: the slice icon, short name and label fields sit
next to AskCompression (both cloned), and the source bookmarks and the
compression checkboxes are both notified.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BPxh5siZR4PzK81twSxnAb
@Ugoyo
Ugoyo requested a review from KnifeLemon November 12, 2026 05:30
@Ugoyo

Ugoyo commented Nov 12, 2026

Copy link
Copy Markdown
Author

Thanks! Both are fixed:

  1. With “Ask every time”, the save location is now asked once for the whole drop and used for every run (AskedCompression.Runs takes it), so a mixed drop gets one dialog and one folder.
  2. The “Ask every time” checkbox now shows only for “same as source” and AskedCompression.LossyImageTargets; a HEIC target keeps its quality slider without it.

I also merged main (#1370, #1389) to clear the conflicts.

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, @Ugoyo! Both points from the last round are in:

  • a mixed drop now asks for the save location once, and every run uses it
  • "Ask every time" only shows for targets the window can estimate, so HEIC keeps its slider without it

We pushed one small commit on top that changes the new Korean strings to the polite 해요체 used across the app. Nothing else changed.

@KnifeLemon
KnifeLemon merged commit 2d0ed90 into KnifeLemon:main Nov 12, 2026
12 checks passed
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]: Add compression tools also

3 participants