Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ body:
attributes:
label: App version
description: Release version or commit hash.
placeholder: v1.2.0
placeholder: v1.3.0
- type: dropdown
id: os
attributes:
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ jobs:

steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0

- name: Setup .NET
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
with:
dotnet-version: 8.0.x

Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ jobs:

steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0

- name: Setup .NET
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
with:
dotnet-version: 8.0.x

Expand All @@ -29,7 +29,7 @@ jobs:
run: |
$tag = $env:RELEASE_TAG
if ($tag -notmatch '^v(?<version>\d+\.\d+\.\d+)$') {
throw "Release tag must use full semantic versioning (example: v1.2.0)."
throw "Release tag must use vMAJOR.MINOR.PATCH (example: v1.3.0)."
}

"tag=$tag" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ coverage/
.DS_Store
Thumbs.db
Desktop.ini
/NUL

# Environment and secrets
.env
Expand Down
30 changes: 29 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ Turn screenshots and images into clean, transparent PNG cut-outs.

Photo Cutter is a privacy-friendly, portable Windows app. It runs as one file, needs no account, and keeps image processing on your computer.

The current stable release is designed for Windows 10 and 11 and focuses on accurate, repeatable cut-outs without changing source-image resolution.

![Photo Cutter editor showing a cut-out workflow](docs/screenshots/photo-cutter-main.png)

## Make your first cut-out

1. Select **Open image** and choose a PNG, JPG, BMP, GIF, or WEBP file.
1. Select **Open** and choose a PNG, JPG, BMP, GIF, or WEBP file.
2. Use **Draw Around**, **Click Around**, or **Shape Cut** to outline what you want to keep.
3. Select **Save Cut-out**, give it a useful name, then export it as a transparent PNG.

Expand All @@ -26,11 +28,13 @@ The labels are deliberately plain English. **Draw Around** is the freehand tool,
- Cut with freehand drawing, connected points, or ready-made shapes.
- Zoom without changing the source image or exported resolution.
- Use a 5x–10x magnifier and choose the cutting-line colour for close edge work.
- Choose Precise, Balanced, or Direct freehand response and Low, Medium, or High line smoothing; hold **Shift** temporarily for extra precision.
- Switch between smooth enlarged viewing and exact-pixel viewing.
- Save an exact-size reusable shape, rename it, place copies, move them, and commit each copy as a new cut-out.
- Rename and reorder committed cut-outs before exporting.
- Refine edges, compare previews, and review results in the Cut-out Gallery.
- Save a workspace and continue later without losing your cut-outs or saved shapes.
- Export selected or all cut-outs with visible progress and a safe Cancel option; completed PNG files are retained.
- Use the complete interface in dark or light appearance.

## Download and requirements
Expand All @@ -42,6 +46,19 @@ The labels are deliberately plain English. **Draw Around** is the freehand tool,

Download the latest release, place `PhotoCutter.exe` wherever you prefer, and open it. The app is currently unsigned, so Windows SmartScreen may ask you to confirm the first launch.

## View settings and workspace data

**View Settings** contains the magnifier level, cutting-line colour, enlarged-preview smoothing, freehand response, freehand smoothing, and light appearance. These preferences are stored locally for the current Windows user.

Workspace files use the `.iusproj` extension. They store cut-outs, reusable shapes, export settings, the linked picture's local path, and a fingerprint of that picture so Photo Cutter can find it again and warn if a different image is selected later. The picture itself is not embedded. Before sharing a workspace file, remember that its stored path may include your Windows username or folder names.

## Known limitations

- GIF input is treated as a still image rather than an animation.
- HEIC, AVIF, TIFF, and SVG input are not currently supported.
- Very large pictures can briefly pause the interface while they are opened and validated.
- The portable executable is not code-signed, so Windows SmartScreen may show a first-launch warning.

## Privacy

Photo Cutter does not upload your images and does not require an account. Cutting, previews, workspaces, and exports are handled locally on your PC.
Expand All @@ -66,6 +83,17 @@ dotnet run --project ".\solution\ImageUiSlicer\ImageUiSlicer.csproj" -c Release

Developer references: [architecture](docs/ARCHITECTURE.md), [behaviour contract](docs/BEHAVIOR_CONTRACT.md), [contributing](.github/CONTRIBUTING.md), and [release process](docs/RELEASE.md).

## Project structure

- `solution/ImageUiSlicer` — WPF application, canvas engine, models, services, workflows, and themes.
- `solution/ImageUiSlicer.Tests` — package-free regression-check executable used locally and in CI.
- `docs` — architecture, behaviour contract, changelog, release notes, and current screenshots.
- `.github` — CI, release automation, contribution guidance, and issue templates.

## Contributing

Read [CONTRIBUTING.md](.github/CONTRIBUTING.md) before proposing a change. Keep source-image geometry independent from display zoom, preserve the shared preview/export renderer, and add a regression check for behaviour changes.

## License

Photo Cutter is available under the [MIT License](LICENSE).
17 changes: 15 additions & 2 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ Models hold workspace and editing data: source metadata, geometry, selections,
cut-outs, saved shapes, export settings, and application settings. Persistent
geometry remains independent of zoom and screen size.

`ProjectExportSettingsModel` keeps the preset, naming mode, and filename prefix
with each workspace. The export destination remains a local user preference and
is deliberately not written into portable workspace files.

Compatibility fields may remain even when they are no longer shown. Removing a
field from the interface is not permission to remove it from saved workspace
data.
Expand All @@ -116,6 +120,7 @@ data.
| `MainViewModel.ShapeTool.cs` | Combined built-in and saved-shape selector |
| `MainViewModel.BrushRefinement.cs` | Edge-refinement stroke state and results |
| `MainViewModel.Export.cs` | Export workflow invocation |
| `MainViewModel.OperationState.cs` | Long-running operation state, progress text, and cancellation |
| `MainViewModel.ExportSettings.cs` | Destination, naming, scale, and outline choices |
| `MainViewModel.ExportPresets.cs` | Preset application and reconciliation |
| `MainViewModel.Preview.cs` | Inspector and gallery preview refresh |
Expand All @@ -132,8 +137,12 @@ while only user-saved shapes expose deletion.
### `Workflows/Exporting`

`ExportBatchCoordinator` owns ordered batch execution, selected-only filtering,
invalid-item skipping, safe numbering, and the final result summary. It calls
`ExportService` for each valid item.
invalid-item skipping, safe numbering, cancellation, progress reporting, and
the final result summary. It calls `ExportService` for each valid item.

`IExportBatchCoordinator`, `ExportBatchResult`, and `ExportProgress` keep the
view model independent from the concrete execution class and separate workflow
input/output state from implementation.

Export presets and editable settings remain in focused view-model partials; the
coordinator is the extracted execution boundary, not a claim that the whole
Expand All @@ -153,6 +162,10 @@ Services own reusable processing and persistence rather than WPF layout:
- `SettingsService` stores local preferences.
- `AtomicFile` provides replace-safe settings and workspace writes.

`Services/Contracts` contains the injected settings, project, and image service
boundaries. `Core/Images/ImageFormatCatalog` is the single source of truth for
the formats accepted by the file picker and decoder.

### `Presentation/Preview`

Preview-specific WPF work is separate from processing services:
Expand Down
30 changes: 30 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,36 @@ The format is based on Keep a Changelog and this project follows Semantic Versio

## [Unreleased]

## [1.3.0] - 2026-08-06

### Added
- Added remembered Precise, Balanced, and Direct freehand response choices plus Low, Medium, and High line smoothing, with **Shift** as a temporary precision override.
- Added cancellable background PNG export with visible per-cut-out progress while retaining files that completed before cancellation.
- Added source-image SHA-256 fingerprints to workspaces so a same-size but different replacement picture can be detected.
- Added a central supported-image-format catalogue and clear guidance for unsupported files.
- Stored each workspace's export preset, naming mode, and filename prefix with the workspace instead of borrowing them from the last-opened project.

### Improved
- Made freehand point spacing consistent at every zoom level and kept the live pointer endpoint visible in both the canvas and precision magnifier.
- Added explicit service and workflow contracts for settings, projects, images, previews, and export coordination, allowing the editor to be tested without writing user settings.
- Split asynchronous command state, export results, progress, and operation state into focused files.
- Expanded regression coverage for high-resolution edges, image identity, workspace repair limits, cancellation, dependency boundaries, and thread-safe image/workspace transfer.
- Pinned CI and release workflow actions to reviewed commit revisions.

### Fixed
- Rejected committed outlines that do not overlap the source image and preserved transparent export padding for cut-outs touching image edges.
- Cancelled interrupted Draw Around gestures cleanly instead of committing partial geometry.
- Restored project path and timestamp state when a workspace save fails.
- Added a geometry-point budget to undo history and aggregate limits to workspace loading to prevent avoidable memory exhaustion.
- Bounded loaded cut-out preview memory, stopped tiny previews being enlarged, and repaired unsafe workspace padding without aborting the open operation.
- Kept image and workspace opening on WPF's owning interface thread after a background-loading experiment caused cross-thread access errors.
- Prevented dropped files from replacing the source image during export, and made cancellation interrupt the in-progress PNG write while cleaning its temporary file.
- Enforced the same 500 cut-out safety limit in the editor, workspace save path, and workspace loader so an over-limit workspace cannot be created accidentally.
- Removed the duplicate-looking visible **100%** toolbar button; native one-pixel viewing remains available with **Ctrl+1**.

### Documentation
- Refreshed the README, architecture notes, release guide, release notes, and application screenshot for the current interface.

## [1.2.0] - 2026-07-17

### Added
Expand Down
4 changes: 2 additions & 2 deletions docs/RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ This repository publishes Photo Cutter through GitHub Releases.
2. Create an annotated semantic version tag and push only that tag:

```powershell
git tag -a v1.2.0 -m "Photo Cutter v1.2.0"
git push origin v1.2.0
git tag -a v1.3.0 -m "Photo Cutter v1.3.0"
git push origin v1.3.0
```

3. GitHub Actions workflow `Release Windows App` will:
Expand Down
26 changes: 26 additions & 0 deletions docs/releases/v1.3.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
## Added

- Precise, Balanced, and Direct freehand response choices, three smoothing levels, and a temporary **Shift** precision override.
- Progress and cancellation when exporting selected or all cut-outs.
- Workspace image fingerprints that warn when a different source picture is selected.
- Workspace-specific export presets, naming modes, and filename prefixes.

## Improved

- Drawing remains responsive and accurately follows the pointer at every zoom level, including inside the precision magnifier.
- Image edges, transparent padding, workspace validation, failed saves, and large undo histories are handled more safely.
- Loaded-workspace thumbnails use bounded memory and no longer enlarge tiny cut-outs.
- Unsupported picture formats now receive a clear explanation; PNG, JPG/JPEG, BMP, GIF, and WEBP remain supported.

## Fixed

- Interrupted freehand drawing no longer commits an unfinished outline.
- Pictures and workspaces open without the WPF cross-thread access error introduced during responsiveness work.
- Dropping another file during export is safely ignored, and cancelling an export removes its unfinished temporary PNG.
- Workspaces now enforce the same 500 cut-out safety limit while editing, saving, and reopening, so the app cannot create a workspace it later refuses to open.
- The duplicate-looking **100%** toolbar button was removed; **Ctrl+1** still selects native one-pixel viewing.

## Known Issues

- GIF files open as still images. HEIC, AVIF, TIFF, and SVG are not supported.
- The portable executable is unsigned, so Windows SmartScreen may request confirmation on first launch.
Binary file modified docs/screenshots/photo-cutter-main.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
81 changes: 81 additions & 0 deletions solution/ImageUiSlicer.Tests/Program.DropChecks.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
using System.IO;
using System.Reflection;
using System.Windows;
using ImageUiSlicer.Views.Dialogs;
using SkiaSharp;

namespace ImageUiSlicer.Tests;

Expand Down Expand Up @@ -27,4 +30,82 @@ private static void MultipleDroppedImagesRequireConfirmedChoice()
choice.ResolveSelection(accepted: true) == secondPath,
"The chooser ignored the user's selected image.");
}

private static void BusyEditorRejectsDroppedFiles()
{
var currentImagePath = Path.Combine(Path.GetTempPath(), $"photo-cutter-current-{Guid.NewGuid():N}.png");
var droppedImagePath = Path.Combine(Path.GetTempPath(), $"photo-cutter-dropped-{Guid.NewGuid():N}.png");
try
{
WriteSolidPng(currentImagePath, 24, 24, SKColors.Coral);
WriteSolidPng(droppedImagePath, 32, 20, SKColors.CornflowerBlue);

var viewModel = CreateViewModel();
using var currentBitmap = new ImageUiSlicer.Services.ImageService().LoadBitmap(currentImagePath);
viewModel.ApplyImageProject(currentImagePath, currentBitmap.Copy());
var window = new ImageUiSlicer.Views.MainWindow(viewModel)
{
ShowActivated = false,
ShowInTaskbar = false,
Opacity = 0,
};
try
{
window.Show();
window.UpdateLayout();
Require(window.AllowDrop, "A ready editor did not accept file drops.");
Require(
viewModel.TryBeginOperation("Working…", CancellationToken.None, out _),
"The drop guard test could not enter the busy state.");
Require(!window.AllowDrop, "The main window still advertised file-drop support while busy.");

var data = new DataObject();
data.SetData(DataFormats.FileDrop, new[] { droppedImagePath });
var constructor = typeof(DragEventArgs).GetConstructor(
BindingFlags.Instance | BindingFlags.NonPublic,
binder: null,
[
typeof(IDataObject),
typeof(DragDropKeyStates),
typeof(DragDropEffects),
typeof(DependencyObject),
typeof(Point),
],
modifiers: null) ?? throw new InvalidOperationException("The WPF drop-event constructor was not found.");
var dropArgs = (DragEventArgs)constructor.Invoke(
[
data,
DragDropKeyStates.None,
DragDropEffects.Copy,
window,
new Point(0, 0),
]);
dropArgs.RoutedEvent = DragDrop.DropEvent;

window.RaiseEvent(dropArgs);

Require(dropArgs.Handled, "The rejected busy-state drop was allowed to continue routing.");
Require(
string.Equals(viewModel.Project.SourceImage.Path, currentImagePath, StringComparison.Ordinal),
"A dropped file replaced the active image during a long-running operation.");

viewModel.CompleteOperation();
Require(window.AllowDrop, "File-drop support did not return after the operation completed.");
}
finally
{
if (viewModel.IsBusy)
{
viewModel.CompleteOperation();
}

window.Close();
}
}
finally
{
File.Delete(currentImagePath);
File.Delete(droppedImagePath);
}
}
}
Loading
Loading