Skip to content

feat(images): Merge images, pictures one under another or side by side (#645) - #682

Merged
KnifeLemon merged 5 commits into
mainfrom
feat/join-images
Oct 27, 2026
Merged

KnifeLemon merged 5 commits into
mainfrom
feat/join-images

Conversation

@KnifeLemon

@KnifeLemon KnifeLemon commented Oct 27, 2026 •

Copy link
Copy Markdown
Owner

What does this change?

Closes #645.

A new preset, Merge images, puts several pictures into one image, one under another or side by side. The front and back of an ID card or a driving licence become one JPG to upload, in one step instead of Merge PDF and then Long image.

It works like Merge videos: dropping two or more pictures on the slice opens the merge order window, where the pictures are sorted (by name, as people count, then by hand with the arrows or Alt+↑ / Alt+↓). For pictures the window has a layout card below the list:

  • Top to bottom / Side by side: the first picture goes at the top, or on the left.
  • Same width (Same height side by side), on by default: pictures of different sizes are scaled to the widest (tallest) one so the edges line up. Off: each keeps its own size, centred.
  • Gap (px): 0 to 1000, a number box without spinner buttons, like the crop window's W × H.
  • Background: a colour picker and a hex box (as on the Theme page), or Transparent. A format without transparency (JPG) gets the preset's usual background colour, white, where it would be see-through.

The button is named after the preset ("Merge images"; for the existing merges it now says "Merge videos" / "Merge audio" instead of always "Merge PDF"). The choices go into the merge and are remembered in the preset for next time; Cancel starts nothing and remembers nothing.

The result is one image named after the first picture, {name}_merged, as Merge videos names its file, in the first picture's format (photo_merged.jpg). A first picture Filee can only read (HEIC, RAW, SVG, ...) gives a PNG. The preset's other picture options (resize, quality, grayscale, size limit) apply to the merged image; rotation and EXIF orientation apply to each picture before it is placed.

How it is built

  • ImageOptions gets JoinIntoOne, JoinDirection, JoinFit, JoinGap and JoinBackground; Preset.JoinsImages says a preset merges pictures. The two enums are new fields with LenientEnumJsonConverter, so a value from a newer Filee reads as the default. Filee 2.1.0 reads such a presets.json: it skips the fields it doesn't know (there the slice converts the pictures one by one).
  • IImageJoiner (Core), implemented by the ImageMagick engine (Magick/ImageJoiner.cs). The layout is worked out from the pictures' sizes first, then each picture is read, scaled and drawn onto the merged image, so only one picture at a time and the result are in memory (a few small pictures are kept from the first read instead of being read twice). Pictures are turned by their EXIF orientation and put into sRGB, so photos with a Display P3 or CMYK profile match.
  • JobQueue.RunMergedImagesAsync: pictures ImageMagick doesn't read as they are (ICNS, JPEG XR, SVG and other drawings) become a plain PNG in the work folder first, without the preset's options, which apply once to the merged image. Writing the one output is shared with Merge videos (WriteJoinedAsync), so naming, progress, cancelling and undoing a failed write are the same.
  • Size limit: the same 200 megapixels as Long image (PdfiumConverter.MaxStackedPixels, about 800 MB while the image is put together). Long image scales a longer stack down; a merged picture is meant to be seen at its size, so a merge beyond that is refused with a translated message and its size, e.g. "The merged picture would be too large, merge fewer or smaller pictures (72000 × 4000 px)". Twelve 24 MP photos side by side are refused, eight fit. The format's own limits (JPG 65,500 px, WebP 16,383 px) scale the result as for every other picture.

Donut placement

Multi-file tools live on back faces, so Merge images goes on the back of the images donut, right before Merge PDF. That face was full (12 slices), so GIF gives its slot: a still picture saved as GIF (256 colours) is the least useful slice there. GIF stays in the library and on the video donut's front, and can be put back in Settings → Toolbar.

Settings schema 53

Main is at 50; #617 takes 51 and another PR 52, so this uses 53. It may need renumbering at merge time depending on what lands first. The step:

  • adds the preset to existing libraries after the media tools, as in a new library;
  • puts it on the images donut's back face before Merge PDF: in a free slot when there is one, else in place of GIF. A face the user laid out without GIF, or with Merge images already on either face, is left alone;
  • puts the option back when a Filee before 53 saved the library since and dropped it (as schemas 31 and 45 do for their presets).

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
  • dotnet build Filee.slnx -c Release
  • dotnet test: Core 664, App 1449, Setup 196, Cli 39 and Platform 86 pass; Engines 1837 pass and 2 failed while every project ran at once (HwpxReaderTests.A_hundred_pages_are_read_in_well_under_a_second, a timing test, and AppLocalRuntimeTests.ProcessRunner_reports_a_missing_DLL_instead_of_the_exit_code); both pass when run again alone and touch nothing here.
    • MergeImagesTests (Core): the preset and its place on the donut, the schema 50 → 53 step (full face, free slot, a face the user laid out, the option put back), the presets file with the new fields and a newer Filee's values, and the queue with a fake joiner (order, first picture's format, PNG fallback, a picture that becomes PNG first without the preset's options, refusals).
    • MergeImagesEngineTests (Engines, real ImageMagick on generated pictures): top to bottom at the same width with a white gap; side by side at their own sizes, centred on a transparent background; a greyscale picture next to a JPG photo with a navy gap; each picture turned before the merge; an SVG merged with a photo; the preset's resize on the merged image; the layout maths; the 200 MP limit; background colours.
    • MergeOrderTests (App, headless): from the donut, the window shows the layout, the choices (side by side, own sizes, gap 6, transparent) give a 156 × 80 PNG and are remembered in presets.json; Cancel starts nothing and remembers nothing. The layout fits at the window's smallest size in en, de, ar (mirrored), ko, ru, hi, ne and th, in light and dark.
  • Tried it in the app: not tried on a desktop yet

Screenshots (UI changes)

Headless renders (MergeOrderTests.Merge_images_layout_fits_at_its_minimum_size, written to tests/Filee.App.Tests/bin/<config>/net10.0/screenshots/merge-images-*.png) at the smallest size the window opens with for pictures (440 × 540): en, de, ar, ko, ru, hi, ne and th, light and dark, each also side by side with a transparent background. Reviewed: every choice whole and inside the card, Arabic mirrored, the list keeps two rows in German and Russian where the layout takes two lines.

Checklist

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

🤖 Generated with Claude Code

#645)

A new preset on the back of the images donut, before Merge PDF (in place of
GIF, which stays in the library). Dropping several pictures on it opens the
merge order window, which for pictures also offers the layout: top to bottom
or side by side, the same width / height or their own sizes centred, a gap in
pixels and a background colour or transparency. The choices are remembered in
the preset.

The ImageMagick engine merges them (IImageJoiner) into one image named after
the first picture ({name}_merged), in its format (PNG when Filee can't write
it). Pictures ImageMagick doesn't read become PNG first. A merged image over
200 megapixels, the size of the longest Long image, is refused with its size.

Settings schema 53 adds the preset to existing libraries and the images
donut's back face. The new layout fields are separate fields with lenient
enum converters, so older presets files and Filee 2.1.0 stay readable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added the feature New features label Oct 27, 2026
KnifeLemon and others added 4 commits October 28, 2026 03:05
A "Pixelate / blur" tool on the images donut opens the pictures one at a time in a window drawn like the crop
overlay: drag frames over names, faces or numbers, each one showing the picture already pixelated or blurred, move
and size them, pick Pixelate or Blur and a strength, then Hide & Save (or Hide & Next). Each picture is written by an
ordinary conversion in its own format (PNG when it can't be written), so the toast, the output rules and the history
work as usual; the frames carry on to the next picture of a batch.

Pixelate averages squares on a grid laid from the picture's corner; blur averages into smaller squares first and
then blurs about a line of text wide, so a light, reversible blur is never used. Both are sized from the picture's
shorter side. The job queue covers the parts once, in the first step whose engine applies image options, refuses a
route that can't, never puts the original back for "only if smaller", and keeps no Retry for them. The previews of
the uncovered picture a file carries (EXIF, Photoshop, XMP thumbnails) are dropped.

Settings schema 52 adds the tool after Crop and on the images donut after Half size (or on the back face when the
front is full). Pixelate or blur and the strength are remembered in the tool, in fields of their own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The toolbar gave up its names once they didn't fit, judged against widths that happened to fit at the window's
default size with Windows' fonts (German with Skip: 978 of 982 px). With CI's Noto fonts they didn't, so the test
that expected the names back at 1040 px failed there. The toolbar now measures each level with the fonts in use and
takes the first that fits: all names; then without the names of Clear all and Skip and the keycaps; then icons only.
The test checks that rule against the measured widths instead of assuming where names fit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…te / blur (52) side by side

Both tools sit after "Half size" on the images donut and after "Crop" in the library, Edit pictures first; the v52
step puts Pixelate / blur after Edit pictures when that is there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…late / blur (52), Merge images (53)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@KnifeLemon
KnifeLemon merged commit 7c718f2 into main Oct 27, 2026
10 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]:merge images

1 participant