diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a655285..bc2803f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,7 +24,7 @@ jobs: uses: microsoft/setup-msbuild@v2 - name: Restore dependencies - run: dotnet restore "Simple Icon File Maker.sln" + run: msbuild "Simple Icon File Maker.sln" /t:Restore /p:Configuration=Release /p:Platform=x64 - name: Build solution - run: dotnet build "Simple Icon File Maker.sln" --configuration Release --no-restore + run: msbuild "Simple Icon File Maker.sln" /p:Configuration=Release /p:Platform=x64 diff --git a/Simple Icon File Maker/Simple Icon File Maker (Package)/Package.appxmanifest b/Simple Icon File Maker/Simple Icon File Maker (Package)/Package.appxmanifest index 8da3f77..17eaf78 100644 --- a/Simple Icon File Maker/Simple Icon File Maker (Package)/Package.appxmanifest +++ b/Simple Icon File Maker/Simple Icon File Maker (Package)/Package.appxmanifest @@ -1,26 +1,28 @@ - + + xmlns:systemai="http://schemas.microsoft.com/appx/manifest/systemai/windows10" + IgnorableNamespaces="uap rescap systemai"> + Version="1.15.0.0" /> + Simple Icon File Maker JoeFinApps Images\StoreLogo.png - - - - + + + + @@ -57,11 +59,24 @@ Images\Image128.png + + + + .png + .jpg + .jpeg + .bmp + .ico + + Bitmap + + + - + \ No newline at end of file diff --git a/Simple Icon File Maker/Simple Icon File Maker (Package)/Simple Icon File Maker (Package).wapproj b/Simple Icon File Maker/Simple Icon File Maker (Package)/Simple Icon File Maker (Package).wapproj index dfb8eb3..4d95bfc 100644 --- a/Simple Icon File Maker/Simple Icon File Maker (Package)/Simple Icon File Maker (Package).wapproj +++ b/Simple Icon File Maker/Simple Icon File Maker (Package)/Simple Icon File Maker (Package).wapproj @@ -36,7 +36,7 @@ 7887a19f-b1cd-4106-a9aa-abaacfe770a9 - 10.0.22621.0 + 10.0.26100.0 10.0.19041.0 10.0.19041.0 net9.0-windows$(TargetPlatformVersion);$(AssetTargetFallback) @@ -51,6 +51,8 @@ True 0 SHA256 + false + false true @@ -120,10 +122,10 @@ - + build - + build diff --git a/Simple Icon File Maker/Simple Icon File Maker/App.xaml.cs b/Simple Icon File Maker/Simple Icon File Maker/App.xaml.cs index b4a692b..286e168 100644 --- a/Simple Icon File Maker/Simple Icon File Maker/App.xaml.cs +++ b/Simple Icon File Maker/Simple Icon File Maker/App.xaml.cs @@ -1,12 +1,17 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.UI.Xaml; +using Microsoft.Windows.AppLifecycle; using Simple_Icon_File_Maker.Activation; using Simple_Icon_File_Maker.Contracts.Services; using Simple_Icon_File_Maker.Models; using Simple_Icon_File_Maker.Services; using Simple_Icon_File_Maker.ViewModels; using Simple_Icon_File_Maker.Views; +using Windows.ApplicationModel.DataTransfer; +using Windows.ApplicationModel.DataTransfer.ShareTarget; +using Windows.Storage; +using Windows.Storage.Streams; namespace Simple_Icon_File_Maker; @@ -37,6 +42,8 @@ public static T GetService() public static UIElement? AppTitlebar { get; set; } + public static string? SharedImagePath { get; set; } + public App() { InitializeComponent(); @@ -93,8 +100,63 @@ private void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledEx protected override async void OnLaunched(LaunchActivatedEventArgs args) { base.OnLaunched(args); + + var activatedEventArgs = AppInstance.GetCurrent().GetActivatedEventArgs(); + if (activatedEventArgs.Kind == ExtendedActivationKind.ShareTarget) + { + await HandleShareTargetActivationAsync(activatedEventArgs); + } + await App.GetService().ActivateAsync(args); } + private static async Task HandleShareTargetActivationAsync(AppActivationArguments activatedEventArgs) + { + if (activatedEventArgs.Data is Windows.ApplicationModel.Activation.IShareTargetActivatedEventArgs shareArgs) + { + ShareOperation shareOperation = shareArgs.ShareOperation; + shareOperation.ReportStarted(); + + try + { + if (shareOperation.Data.Contains(StandardDataFormats.StorageItems)) + { + IReadOnlyList items = await shareOperation.Data.GetStorageItemsAsync(); + foreach (IStorageItem item in items) + { + if (item is StorageFile file && Constants.FileTypes.SupportedImageFormats.Contains(file.FileType, StringComparer.OrdinalIgnoreCase)) + { + StorageFolder tempFolder = ApplicationData.Current.TemporaryFolder; + StorageFile copiedFile = await file.CopyAsync(tempFolder, file.Name, NameCollisionOption.GenerateUniqueName); + SharedImagePath = copiedFile.Path; + break; + } + } + } + else if (shareOperation.Data.Contains(StandardDataFormats.Bitmap)) + { + var bitmapRef = await shareOperation.Data.GetBitmapAsync(); + var stream = await bitmapRef.OpenReadAsync(); + + StorageFolder tempFolder = ApplicationData.Current.TemporaryFolder; + StorageFile tempFile = await tempFolder.CreateFileAsync("shared_image.png", CreationCollisionOption.GenerateUniqueName); + + using (var outputStream = await tempFile.OpenAsync(FileAccessMode.ReadWrite)) + { + await RandomAccessStream.CopyAsync(stream, outputStream); + } + + SharedImagePath = tempFile.Path; + } + } + catch (Exception) + { + // If share handling fails, continue with normal launch + } + + shareOperation.ReportCompleted(); + } + } + public static string[]? cliArgs { get; } = Environment.GetCommandLineArgs(); -} +} \ No newline at end of file diff --git a/Simple Icon File Maker/Simple Icon File Maker/FodyWeavers.xml b/Simple Icon File Maker/Simple Icon File Maker/FodyWeavers.xml deleted file mode 100644 index d5abfed..0000000 --- a/Simple Icon File Maker/Simple Icon File Maker/FodyWeavers.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/Simple Icon File Maker/Simple Icon File Maker/Helpers/BackgroundRemoverHelper.cs b/Simple Icon File Maker/Simple Icon File Maker/Helpers/BackgroundRemoverHelper.cs new file mode 100644 index 0000000..994c091 --- /dev/null +++ b/Simple Icon File Maker/Simple Icon File Maker/Helpers/BackgroundRemoverHelper.cs @@ -0,0 +1,104 @@ +using Microsoft.Windows.AI; +using Microsoft.Windows.AI.Imaging; +using System.Runtime.InteropServices.WindowsRuntime; +using Windows.Graphics; +using Windows.Graphics.Imaging; +using Windows.Storage; +using Windows.Storage.Streams; + +namespace Simple_Icon_File_Maker.Helpers; + +public static class BackgroundRemoverHelper +{ + public static async Task IsAvailableAsync() + { + try + { + AIFeatureReadyState readyState = ImageObjectExtractor.GetReadyState(); + + if (readyState == AIFeatureReadyState.Ready) + return true; + + if (readyState == AIFeatureReadyState.NotReady) + { + var result = await ImageObjectExtractor.EnsureReadyAsync(); + return result.Status == AIFeatureReadyResultState.Success; + } + + // NotSupportedOnCurrentSystem or DisabledByUser + return false; + } + catch + { + return false; + } + } + + public static async Task RemoveBackgroundAsync(string imagePath) + { + StorageFile inputFile = await StorageFile.GetFileFromPathAsync(imagePath); + SoftwareBitmap sourceBitmap; + + using (IRandomAccessStream stream = await inputFile.OpenAsync(FileAccessMode.Read)) + { + BitmapDecoder decoder = await BitmapDecoder.CreateAsync(stream); + sourceBitmap = await decoder.GetSoftwareBitmapAsync(BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied); + } + + using ImageObjectExtractor extractor = await ImageObjectExtractor.CreateWithSoftwareBitmapAsync(sourceBitmap); + + // Hint with the entire image rect as the region of interest + ImageObjectExtractorHint hint = new( + includeRects: [new RectInt32(0, 0, sourceBitmap.PixelWidth, sourceBitmap.PixelHeight)], + includePoints: [], + excludePoints: []); + + SoftwareBitmap mask = extractor.GetSoftwareBitmapObjectMask(hint); + + SoftwareBitmap resultBitmap = ApplyMaskAsAlpha(sourceBitmap, mask); + + StorageFolder cacheFolder = ApplicationData.Current.LocalCacheFolder; + string fileName = Path.GetFileNameWithoutExtension(imagePath); + string outputFileName = $"{fileName}_nobg.png"; + StorageFile outputFile = await cacheFolder.CreateFileAsync(outputFileName, CreationCollisionOption.ReplaceExisting); + + using (IRandomAccessStream outputStream = await outputFile.OpenAsync(FileAccessMode.ReadWrite)) + { + BitmapEncoder encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, outputStream); + encoder.SetSoftwareBitmap(resultBitmap); + await encoder.FlushAsync(); + } + + return outputFile.Path; + } + + private static SoftwareBitmap ApplyMaskAsAlpha(SoftwareBitmap original, SoftwareBitmap mask) + { + int width = original.PixelWidth; + int height = original.PixelHeight; + + SoftwareBitmap gray = mask.BitmapPixelFormat == BitmapPixelFormat.Gray8 + ? mask + : SoftwareBitmap.Convert(mask, BitmapPixelFormat.Gray8); + + byte[] originalPixels = new byte[4 * width * height]; + byte[] maskPixels = new byte[width * height]; + original.CopyToBuffer(originalPixels.AsBuffer()); + gray.CopyToBuffer(maskPixels.AsBuffer()); + + byte[] resultPixels = new byte[4 * width * height]; + for (int i = 0; i < maskPixels.Length; i++) + { + int px = i * 4; + int m = 255 - maskPixels[i]; + resultPixels[px + 0] = (byte)(originalPixels[px + 0] * m / 255); + resultPixels[px + 1] = (byte)(originalPixels[px + 1] * m / 255); + resultPixels[px + 2] = (byte)(originalPixels[px + 2] * m / 255); + resultPixels[px + 3] = (byte)(originalPixels[px + 3] * m / 255); + } + + SoftwareBitmap result = new(BitmapPixelFormat.Bgra8, width, height, BitmapAlphaMode.Premultiplied); + result.CopyFromBuffer(resultPixels.AsBuffer()); + return result; + } +} \ No newline at end of file diff --git a/Simple Icon File Maker/Simple Icon File Maker/Helpers/FilePickerHelper.cs b/Simple Icon File Maker/Simple Icon File Maker/Helpers/FilePickerHelper.cs index c6bba32..53b3eac 100644 --- a/Simple Icon File Maker/Simple Icon File Maker/Helpers/FilePickerHelper.cs +++ b/Simple Icon File Maker/Simple Icon File Maker/Helpers/FilePickerHelper.cs @@ -17,6 +17,8 @@ public static async Task TrySetSuggestedFolderFromSourceImage(FileSavePicker sav if (File.Exists(imagePath)) { StorageFile sourceFile = await StorageFile.GetFileFromPathAsync(imagePath); + string name = Path.GetFileNameWithoutExtension(imagePath); + await sourceFile.RenameAsync(name); savePicker.SuggestedSaveFile = sourceFile; } } @@ -25,4 +27,4 @@ public static async Task TrySetSuggestedFolderFromSourceImage(FileSavePicker sav // If file access fails, fall back to default picker behavior } } -} +} \ No newline at end of file diff --git a/Simple Icon File Maker/Simple Icon File Maker/Models/IconSize.cs b/Simple Icon File Maker/Simple Icon File Maker/Models/IconSize.cs index a709b99..a650808 100644 --- a/Simple Icon File Maker/Simple Icon File Maker/Models/IconSize.cs +++ b/Simple Icon File Maker/Simple Icon File Maker/Models/IconSize.cs @@ -1,20 +1,26 @@ -using System.ComponentModel; +using CommunityToolkit.Mvvm.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; namespace Simple_Icon_File_Maker.Models; [DebuggerDisplay("SideLength = {SideLength}, IsSelected = {IsSelected}")] -public partial class IconSize : INotifyPropertyChanged, IEquatable +public partial class IconSize : ObservableObject, IEquatable { - public int SideLength { get; set; } - public bool IsSelected { get; set; } = true; + [ObservableProperty] + public partial int SideLength { get; set; } - public bool IsEnabled { get; set; } = true; + [ObservableProperty] + public partial bool IsSelected { get; set; } = true; - public bool IsHidden { get; set; } = false; + [ObservableProperty] + public partial bool IsEnabled { get; set; } = true; - public int Order { get; set; } = 0; + [ObservableProperty] + public partial bool IsHidden { get; set; } = false; + + [ObservableProperty] + public partial int Order { get; set; } = 0; public string Tooltip => $"{SideLength} x {SideLength}"; @@ -53,10 +59,6 @@ public IconSize(IconSize iconSize) Order = iconSize.Order; } -#pragma warning disable CS0067 // The event 'IconSize.PropertyChanged' is never used - public event PropertyChangedEventHandler? PropertyChanged; -#pragma warning restore CS0067 // The event 'IconSize.PropertyChanged' is never used - public static IconSize[] GetAllSizes() { return @@ -165,4 +167,4 @@ public int GetHashCode([DisallowNull] IconSize obj) { return obj.GetHashCode(); } -} +} \ No newline at end of file diff --git a/Simple Icon File Maker/Simple Icon File Maker/Simple Icon File Maker.csproj b/Simple Icon File Maker/Simple Icon File Maker/Simple Icon File Maker.csproj index 08b1c8c..04d3efe 100644 --- a/Simple Icon File Maker/Simple Icon File Maker/Simple Icon File Maker.csproj +++ b/Simple Icon File Maker/Simple Icon File Maker/Simple Icon File Maker.csproj @@ -1,8 +1,8 @@ - + WinExe - net9.0-windows10.0.22621.0 - 10.0.22621.38 + net9.0-windows10.0.26100.0 + 10.0.26100.38 10.0.19041.0 10.0.19041.0 Simple_Icon_File_Maker @@ -13,9 +13,9 @@ False False SimpleIconMaker.ico - Joseph Finney 2024 + Joseph Finney 2026 Image128.png - enable + enable enable true false @@ -31,6 +31,7 @@ + @@ -39,15 +40,14 @@ - - - - + + + + - - - - + + + @@ -81,6 +81,9 @@ $(DefaultXamlRuntime) + + $(DefaultXamlRuntime) + diff --git a/Simple Icon File Maker/Simple Icon File Maker/ViewModels/MainViewModel.cs b/Simple Icon File Maker/Simple Icon File Maker/ViewModels/MainViewModel.cs index 1935e9e..ab962c9 100644 --- a/Simple Icon File Maker/Simple Icon File Maker/ViewModels/MainViewModel.cs +++ b/Simple Icon File Maker/Simple Icon File Maker/ViewModels/MainViewModel.cs @@ -1,4 +1,3 @@ -using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using ImageMagick; using Microsoft.UI.Xaml; @@ -152,8 +151,14 @@ public async void OnNavigatedTo(object parameter) ShowUpgradeToProButton = !_storeService.OwnsPro; + // Load shared image path from share target activation + if (!string.IsNullOrEmpty(App.SharedImagePath)) + { + ImagePath = App.SharedImagePath; + App.SharedImagePath = null; + } // Load CLI args if present - if (App.cliArgs?.Length > 1) + else if (App.cliArgs?.Length > 1) { ImagePath = App.cliArgs[1]; } @@ -605,6 +610,38 @@ public async Task ApplyInvert() } } + [RelayCommand] + public async Task RemoveBackground() + { + if (string.IsNullOrWhiteSpace(ImagePath)) + return; + + try + { + RemoveBackgroundDialog dialog = new(ImagePath); + await NavigationService.ShowModal(dialog); + + if (dialog.ResultImagePath is not null) + { + ImageMagick.MagickImage resultImage = new(dialog.ResultImagePath); + if (MainImage != null) + MainImage.Source = resultImage.ToImageSource(); + + MagickImageUndoRedoItem undoRedoItem = new(MainImage!, ImagePath, dialog.ResultImagePath); + _undoRedo.AddUndo(undoRedoItem); + UpdateUndoRedoState(); + + ImagePath = dialog.ResultImagePath; + await RefreshPreviews(); + } + } + catch (Exception ex) + { + Debug.WriteLine($"Failed to remove background: {ex.Message}"); + ShowError($"Failed to remove background: {ex.Message}"); + } + } + [RelayCommand] public async Task Undo() { diff --git a/Simple Icon File Maker/Simple Icon File Maker/Views/AboutPage.xaml b/Simple Icon File Maker/Simple Icon File Maker/Views/AboutPage.xaml index 6af0196..847b30a 100644 --- a/Simple Icon File Maker/Simple Icon File Maker/Views/AboutPage.xaml +++ b/Simple Icon File Maker/Simple Icon File Maker/Views/AboutPage.xaml @@ -1,4 +1,4 @@ - @@ -165,7 +165,7 @@ + NavigateUri="https://github.com/microsoft/microsoft-ui-xaml"> WinUI 3 - + \ No newline at end of file diff --git a/Simple Icon File Maker/Simple Icon File Maker/Views/MainPage.xaml b/Simple Icon File Maker/Simple Icon File Maker/Views/MainPage.xaml index 84b354d..8b740c7 100644 --- a/Simple Icon File Maker/Simple Icon File Maker/Views/MainPage.xaml +++ b/Simple Icon File Maker/Simple Icon File Maker/Views/MainPage.xaml @@ -1,4 +1,3 @@ - + + diff --git a/Simple Icon File Maker/Simple Icon File Maker/Views/RemoveBackgroundDialog.xaml b/Simple Icon File Maker/Simple Icon File Maker/Views/RemoveBackgroundDialog.xaml new file mode 100644 index 0000000..47b413e --- /dev/null +++ b/Simple Icon File Maker/Simple Icon File Maker/Views/RemoveBackgroundDialog.xaml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Simple Icon File Maker/Simple Icon File Maker/Views/RemoveBackgroundDialog.xaml.cs b/Simple Icon File Maker/Simple Icon File Maker/Views/RemoveBackgroundDialog.xaml.cs new file mode 100644 index 0000000..2a62eda --- /dev/null +++ b/Simple Icon File Maker/Simple Icon File Maker/Views/RemoveBackgroundDialog.xaml.cs @@ -0,0 +1,63 @@ +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media.Imaging; +using Simple_Icon_File_Maker.Helpers; + +namespace Simple_Icon_File_Maker; + +public sealed partial class RemoveBackgroundDialog : ContentDialog +{ + public string? ResultImagePath { get; private set; } + + private readonly string _imagePath; + private string? _pendingResultPath; + + public RemoveBackgroundDialog(string imagePath) + { + InitializeComponent(); + _imagePath = imagePath; + PrimaryButtonClick += OnPrimaryButtonClick; + } + + private void OnPrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args) + { + ResultImagePath = _pendingResultPath; + } + + private async void ContentDialog_Loaded(object sender, RoutedEventArgs e) + { + BeforeImage.Source = new BitmapImage(new Uri(_imagePath)); + + bool isAvailable = await BackgroundRemoverHelper.IsAvailableAsync(); + if (!isAvailable) + { + StatusInfoBar.Title = "Not Available"; + StatusInfoBar.Message = "The AI background removal model is not available on this device. This feature requires a Copilot+ PC with the latest Windows updates."; + StatusInfoBar.Severity = InfoBarSeverity.Warning; + StatusInfoBar.IsOpen = true; + ProcessingRing.IsActive = false; + return; + } + + try + { + string resultPath = await BackgroundRemoverHelper.RemoveBackgroundAsync(_imagePath); + _pendingResultPath = resultPath; + + AfterImage.Source = new BitmapImage(new Uri(resultPath)); + + IsPrimaryButtonEnabled = true; + } + catch (Exception ex) + { + StatusInfoBar.Title = "Error"; + StatusInfoBar.Message = $"Failed to remove background: {ex.Message}"; + StatusInfoBar.Severity = InfoBarSeverity.Error; + StatusInfoBar.IsOpen = true; + } + finally + { + ProcessingRing.IsActive = false; + } + } +} \ No newline at end of file