diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a655285..d9a4e06 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: dotnet restore "Simple Icon File Maker/Simple Icon File Maker/Simple Icon File Maker.csproj" - - name: Build solution - run: dotnet build "Simple Icon File Maker.sln" --configuration Release --no-restore + - name: Build + run: dotnet build "Simple Icon File Maker/Simple Icon File Maker/Simple Icon File Maker.csproj" --configuration Release --no-restore -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..d9637f5 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 + + + 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..20445a7 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 @@ -1,132 +1,134 @@ - - 15.0 - - - - Debug - x86 - - - Release - x86 - - - Debug - x64 - - - Release - x64 - - - Debug - arm64 - - - Release - arm64 - - - - $(MSBuildExtensionsPath)\Microsoft\DesktopBridge\ - Simple Icon File Maker\ - - - - 7887a19f-b1cd-4106-a9aa-abaacfe770a9 - 10.0.22621.0 - 10.0.19041.0 - 10.0.19041.0 - net9.0-windows$(TargetPlatformVersion);$(AssetTargetFallback) - en-US - false - ..\Simple Icon File Maker\Simple Icon File Maker.csproj - False - False - True - x86|x64|arm64 - x86|x64|arm64 - True - 0 - SHA256 - true - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - - Designer - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - True - Properties\PublishProfiles\win-$(Platform).pubxml - - - - - build - - - build - - - - + + 15.0 + + + + Debug + x86 + + + Release + x86 + + + Debug + x64 + + + Release + x64 + + + Debug + arm64 + + + Release + arm64 + + + + $(MSBuildExtensionsPath)\Microsoft\DesktopBridge\ + Simple Icon File Maker\ + + + + 7887a19f-b1cd-4106-a9aa-abaacfe770a9 + 10.0.26100.0 + 10.0.19041.0 + 10.0.19041.0 + net9.0-windows$(TargetPlatformVersion);$(AssetTargetFallback) + en-US + false + ..\Simple Icon File Maker\Simple Icon File Maker.csproj + False + False + True + x86|x64|arm64 + x86|x64|arm64 + True + 0 + SHA256 + false + false + true + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + + Designer + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + True + Properties\PublishProfiles\win-$(Platform).pubxml + + + + + build + + + build + + + + \ No newline at end of file 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..f3c010a 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.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(); } 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..3889f5c --- /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; + } +} 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..bfe9695 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; } } 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..07b4369 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 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..a4559fd 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..091cc94 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 @@ -152,8 +152,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 +611,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..a6cd53b 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 @@ -25,7 +25,7 @@ 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..43da06d 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 @@ -289,6 +289,11 @@ x:Name="InvertButton" Command="{x:Bind ViewModel.ApplyInvertCommand}" Text="Invert" /> + + 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..ff7a949 --- /dev/null +++ b/Simple Icon File Maker/Simple Icon File Maker/Views/RemoveBackgroundDialog.xaml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..be7f0d6 --- /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; + } + } +}