diff --git a/StabilityMatrix.Avalonia/App.axaml.cs b/StabilityMatrix.Avalonia/App.axaml.cs index 742b10a3..08e8ba51 100644 --- a/StabilityMatrix.Avalonia/App.axaml.cs +++ b/StabilityMatrix.Avalonia/App.axaml.cs @@ -80,7 +80,8 @@ public sealed class App : Application public static TopLevel TopLevel => TopLevel.GetTopLevel(VisualRoot)!; - internal static bool IsHeadlessMode => TopLevel.TryGetPlatformHandle()?.HandleDescriptor is null or "STUB"; + internal static bool IsHeadlessMode => + TopLevel.TryGetPlatformHandle()?.HandleDescriptor is null or "STUB"; [NotNull] public static IStorageProvider? StorageProvider { get; internal set; } @@ -117,8 +118,7 @@ public sealed class App : Application { // Remove DataAnnotations validation plugin since we're using INotifyDataErrorInfo from MvvmToolkit var dataValidationPluginsToRemove = BindingPlugins - .DataValidators - .OfType() + .DataValidators.OfType() .ToArray(); foreach (var plugin in dataValidationPluginsToRemove) @@ -161,22 +161,19 @@ public sealed class App : Application DesktopLifetime.MainWindow = setupWindow; - setupWindow - .ShowAsyncCts - .Token - .Register(() => + setupWindow.ShowAsyncCts.Token.Register(() => + { + if (setupWindow.Result == ContentDialogResult.Primary) + { + settingsManager.SetEulaAccepted(); + ShowMainWindow(); + DesktopLifetime.MainWindow.Show(); + } + else { - if (setupWindow.Result == ContentDialogResult.Primary) - { - settingsManager.SetEulaAccepted(); - ShowMainWindow(); - DesktopLifetime.MainWindow.Show(); - } - else - { - Shutdown(); - } - }); + Shutdown(); + } + }); } else { @@ -297,7 +294,9 @@ public sealed class App : Application var serviceManager = new ServiceManager(); var serviceManagedTypes = exportedTypes - .Select(t => new { t, attributes = t.GetCustomAttributes(typeof(ManagedServiceAttribute), true) }) + .Select( + t => new { t, attributes = t.GetCustomAttributes(typeof(ManagedServiceAttribute), true) } + ) .Where(t1 => t1.attributes is { Length: > 0 }) .Select(t1 => t1.t) .ToList(); @@ -322,8 +321,7 @@ public sealed class App : Application services.AddMessagePipeNamedPipeInterprocess("StabilityMatrix"); var exportedTypes = AppDomain - .CurrentDomain - .GetAssemblies() + .CurrentDomain.GetAssemblies() .Where(a => a.FullName?.StartsWith("StabilityMatrix") == true) .SelectMany(a => a.GetExportedTypes()) .ToArray(); @@ -332,7 +330,8 @@ public sealed class App : Application .Select(t => new { t, attributes = t.GetCustomAttributes(typeof(TransientAttribute), false) }) .Where( t1 => - t1.attributes is { Length: > 0 } && !t1.t.Name.Contains("Mock", StringComparison.OrdinalIgnoreCase) + t1.attributes is { Length: > 0 } + && !t1.t.Name.Contains("Mock", StringComparison.OrdinalIgnoreCase) ) .Select(t1 => new { Type = t1.t, Attribute = (TransientAttribute)t1.attributes[0] }); @@ -352,9 +351,12 @@ public sealed class App : Application .Select(t => new { t, attributes = t.GetCustomAttributes(typeof(SingletonAttribute), false) }) .Where( t1 => - t1.attributes is { Length: > 0 } && !t1.t.Name.Contains("Mock", StringComparison.OrdinalIgnoreCase) + t1.attributes is { Length: > 0 } + && !t1.t.Name.Contains("Mock", StringComparison.OrdinalIgnoreCase) ) - .Select(t1 => new { Type = t1.t, Attributes = t1.attributes.Cast().ToArray() }); + .Select( + t1 => new { Type = t1.t, Attributes = t1.attributes.Cast().ToArray() } + ); foreach (var typePair in singletonTypes) { @@ -386,7 +388,9 @@ public sealed class App : Application // Rich presence services.AddSingleton(); - services.AddSingleton(provider => provider.GetRequiredService()); + services.AddSingleton( + provider => provider.GetRequiredService() + ); Config = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) @@ -557,7 +561,11 @@ public sealed class App : Application #if DEBUG builder.AddNLog( ConfigureLogging(), - new NLogProviderOptions { IgnoreEmptyEventId = false, CaptureEventId = EventIdCaptureType.Legacy } + new NLogProviderOptions + { + IgnoreEmptyEventId = false, + CaptureEventId = EventIdCaptureType.Legacy + } ); #else builder.AddNLog(ConfigureLogging()); @@ -591,8 +599,6 @@ public sealed class App : Application if (e.Cancel) return; - var mainWindow = (MainWindow)sender!; - // Show confirmation if package running var launchPageViewModel = Services.GetRequiredService(); launchPageViewModel.OnMainWindowClosing(e); @@ -602,66 +608,38 @@ public sealed class App : Application // Check if we need to dispose IAsyncDisposables if ( - !isAsyncDisposeComplete - && Services.GetServices().ToList() is { Count: > 0 } asyncDisposables + isAsyncDisposeComplete + || Services.GetServices().ToList() is not { Count: > 0 } asyncDisposables ) - { - // Cancel shutdown for now - e.Cancel = true; - isAsyncDisposeComplete = true; + return; - Debug.WriteLine("OnShutdownRequested Canceled: Disposing IAsyncDisposables"); + // Cancel shutdown for now + e.Cancel = true; + isAsyncDisposeComplete = true; - Task.Run(async () => + Debug.WriteLine("OnShutdownRequested Canceled: Disposing IAsyncDisposables"); + + Task.Run(async () => + { + foreach (var disposable in asyncDisposables) { - foreach (var disposable in asyncDisposables) + Debug.WriteLine($"Disposing IAsyncDisposable ({disposable.GetType().Name})"); + try { - Debug.WriteLine($"Disposing IAsyncDisposable ({disposable.GetType().Name})"); - try - { - await disposable.DisposeAsync().ConfigureAwait(false); - } - catch (Exception ex) - { - Debug.Fail(ex.ToString()); - } + await disposable.DisposeAsync().ConfigureAwait(false); } - }) - .ContinueWith(_ => - { - // Shutdown again - Dispatcher.UIThread.Invoke(() => Shutdown()); - }) - .SafeFireAndForget(); - - return; - } - - OnMainWindowClosingTerminal(mainWindow); - } - - /// - /// Called at the end of before the main window is closed. - /// - private static void OnMainWindowClosingTerminal(Window sender) - { - var settingsManager = Services.GetRequiredService(); - - // Save window position - var validWindowPosition = sender.Screens.All.Any(screen => screen.Bounds.Contains(sender.Position)); - - settingsManager.Transaction( - s => + catch (Exception ex) + { + Debug.Fail(ex.ToString()); + } + } + }) + .ContinueWith(_ => { - s.WindowSettings = new WindowSettings( - sender.Width, - sender.Height, - validWindowPosition ? sender.Position.X : 0, - validWindowPosition ? sender.Position.Y : 0 - ); - }, - ignoreMissingLibraryDir: true - ); + // Shutdown again + Dispatcher.UIThread.Invoke(() => Shutdown()); + }) + .SafeFireAndForget(); } private static void OnExit(object? sender, ControlledApplicationLifetimeExitEventArgs args) @@ -714,10 +692,12 @@ public sealed class App : Application .WriteTo( new FileTarget { - Layout = "${longdate}|${level:uppercase=true}|${logger}|${message:withexception=true}", + Layout = + "${longdate}|${level:uppercase=true}|${logger}|${message:withexception=true}", ArchiveOldFileOnStartup = true, FileName = "${specialfolder:folder=ApplicationData}/StabilityMatrix/app.log", - ArchiveFileName = "${specialfolder:folder=ApplicationData}/StabilityMatrix/app.{#}.log", + ArchiveFileName = + "${specialfolder:folder=ApplicationData}/StabilityMatrix/app.{#}.log", ArchiveNumbering = ArchiveNumberingMode.Rolling, MaxArchiveFiles = 2 } @@ -730,7 +710,9 @@ public sealed class App : Application builder.ForLogger("Microsoft.Extensions.Http.*").WriteToNil(NLog.LogLevel.Warn); // Disable console trace logging by default - builder.ForLogger("StabilityMatrix.Avalonia.ViewModels.ConsoleViewModel").WriteToNil(NLog.LogLevel.Debug); + builder + .ForLogger("StabilityMatrix.Avalonia.ViewModels.ConsoleViewModel") + .WriteToNil(NLog.LogLevel.Debug); // Disable LoadableViewModelBase trace logging by default builder @@ -751,20 +733,18 @@ public sealed class App : Application // Sentry if (SentrySdk.IsEnabled) { - LogManager - .Configuration - .AddSentry(o => - { - o.InitializeSdk = false; - o.Layout = "${message}"; - o.ShutdownTimeoutSeconds = 5; - o.IncludeEventDataOnBreadcrumbs = true; - o.BreadcrumbLayout = "${logger}: ${message}"; - // Debug and higher are stored as breadcrumbs (default is Info) - o.MinimumBreadcrumbLevel = NLog.LogLevel.Debug; - // Error and higher is sent as event (default is Error) - o.MinimumEventLevel = NLog.LogLevel.Error; - }); + LogManager.Configuration.AddSentry(o => + { + o.InitializeSdk = false; + o.Layout = "${message}"; + o.ShutdownTimeoutSeconds = 5; + o.IncludeEventDataOnBreadcrumbs = true; + o.BreadcrumbLayout = "${logger}: ${message}"; + // Debug and higher are stored as breadcrumbs (default is Info) + o.MinimumBreadcrumbLevel = NLog.LogLevel.Debug; + // Error and higher is sent as event (default is Error) + o.MinimumEventLevel = NLog.LogLevel.Error; + }); } LogManager.ReconfigExistingLoggers(); @@ -803,34 +783,36 @@ public sealed class App : Application results.Add(ms); } - Dispatcher - .UIThread - .InvokeAsync(async () => - { - var dest = await StorageProvider.SaveFilePickerAsync( - new FilePickerSaveOptions() { SuggestedFileName = "screenshot.png", ShowOverwritePrompt = true } - ); + Dispatcher.UIThread.InvokeAsync(async () => + { + var dest = await StorageProvider.SaveFilePickerAsync( + new FilePickerSaveOptions() + { + SuggestedFileName = "screenshot.png", + ShowOverwritePrompt = true + } + ); - if (dest?.TryGetLocalPath() is { } localPath) + if (dest?.TryGetLocalPath() is { } localPath) + { + var localFile = new FilePath(localPath); + foreach (var (i, stream) in results.Enumerate()) { - var localFile = new FilePath(localPath); - foreach (var (i, stream) in results.Enumerate()) + var name = localFile.NameWithoutExtension; + if (results.Count > 1) { - var name = localFile.NameWithoutExtension; - if (results.Count > 1) - { - name += $"_{i + 1}"; - } - - localFile = localFile.Directory!.JoinFile(name + ".png"); - localFile.Create(); - - await using var fileStream = localFile.Info.OpenWrite(); - stream.Seek(0, SeekOrigin.Begin); - await stream.CopyToAsync(fileStream); + name += $"_{i + 1}"; } + + localFile = localFile.Directory!.JoinFile(name + ".png"); + localFile.Create(); + + await using var fileStream = localFile.Info.OpenWrite(); + stream.Seek(0, SeekOrigin.Begin); + await stream.CopyToAsync(fileStream); } - }); + } + }); } [Conditional("DEBUG")] diff --git a/StabilityMatrix.Avalonia/FallbackRamCachedWebImageLoader.cs b/StabilityMatrix.Avalonia/FallbackRamCachedWebImageLoader.cs index f8ae8235..7dc26555 100644 --- a/StabilityMatrix.Avalonia/FallbackRamCachedWebImageLoader.cs +++ b/StabilityMatrix.Avalonia/FallbackRamCachedWebImageLoader.cs @@ -1,12 +1,15 @@ using System; using System.Collections.Concurrent; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Linq; using System.Threading.Tasks; using AsyncAwaitBestPractices; using AsyncImageLoader.Loaders; using Avalonia.Media.Imaging; using StabilityMatrix.Core.Extensions; +using StabilityMatrix.Core.Helper; namespace StabilityMatrix.Avalonia; @@ -42,6 +45,11 @@ public class FallbackRamCachedWebImageLoader : RamCachedWebImageLoader { try { + if (url.EndsWith("png", StringComparison.OrdinalIgnoreCase)) + { + return await LoadPngAsync(url); + } + return new Bitmap(url); } catch (Exception e) @@ -98,4 +106,14 @@ public class FallbackRamCachedWebImageLoader : RamCachedWebImageLoader } } } + + private async Task LoadPngAsync(string url) + { + using var fileStream = new BinaryReader(File.OpenRead(url)); + var imageBytes = ImageMetadata.BuildImageWithoutMetadata(fileStream).ToArray(); + using var memoryStream = new MemoryStream(); + await memoryStream.WriteAsync(imageBytes, 0, imageBytes.Length); + memoryStream.Position = 0; + return new Bitmap(memoryStream); + } } diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/Video/SvdImgToVidConditioningViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/Video/SvdImgToVidConditioningViewModel.cs index 4932ad55..ae4cbcc5 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Inference/Video/SvdImgToVidConditioningViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Inference/Video/SvdImgToVidConditioningViewModel.cs @@ -41,13 +41,15 @@ public partial class SvdImgToVidConditioningViewModel public void LoadStateFromParameters(GenerationParameters parameters) { - // TODO + Width = parameters.Width; + Height = parameters.Height; + // TODO: add more metadata } public GenerationParameters SaveStateToParameters(GenerationParameters parameters) { - // TODO - return parameters; + // TODO: add more metadata + return parameters with { Width = Width, Height = Height, }; } public void ApplyStep(ModuleApplyStepEventArgs e) @@ -58,8 +60,7 @@ public partial class SvdImgToVidConditioningViewModel { Name = e.Nodes.GetUniqueName("LinearCfgGuidance"), Model = - e.Builder.Connections.BaseModel - ?? throw new ValidationException("Model not selected"), + e.Builder.Connections.BaseModel ?? throw new ValidationException("Model not selected"), MinCfg = MinCfg } ); diff --git a/StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs index 842df99e..c546d1ee 100644 --- a/StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Reactive.Linq; using System.Threading.Tasks; using AsyncAwaitBestPractices; using Avalonia.Controls; @@ -10,6 +11,7 @@ using Avalonia.Controls.Primitives; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using FluentAvalonia.UI.Controls; +using KGySoft.CoreLibraries; using NLog; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Services; @@ -112,10 +114,7 @@ public partial class MainWindowViewModel : ViewModelBase var startupTime = CodeTimer.FormatTime(Program.StartupTimer.Elapsed); Logger.Info($"App started ({startupTime})"); - if ( - Program.Args.DebugOneClickInstall - || settingsManager.Settings.InstalledPackages.Count == 0 - ) + if (Program.Args.DebugOneClickInstall || settingsManager.Settings.InstalledPackages.Count == 0) { var viewModel = dialogFactory.Get(); var dialog = new BetterContentDialog @@ -148,8 +147,8 @@ public partial class MainWindowViewModel : ViewModelBase .Where(p => p.GetType().GetCustomAttributes(typeof(PreloadAttribute), true).Any()) ) { - Dispatcher.UIThread - .InvokeAsync( + Dispatcher + .UIThread.InvokeAsync( async () => { var stopwatch = Stopwatch.StartNew(); diff --git a/StabilityMatrix.Avalonia/ViewModels/Settings/MainSettingsViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Settings/MainSettingsViewModel.cs index d1e25903..36d97593 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Settings/MainSettingsViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Settings/MainSettingsViewModel.cs @@ -24,6 +24,7 @@ using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using DynamicData.Binding; +using ExifLibrary; using FluentAvalonia.UI.Controls; using NLog; using SkiaSharp; @@ -708,6 +709,10 @@ public partial class MainSettingsViewModel : PageViewModelBase if (files.Count == 0) return; + var data = await ImageMetadata.ReadTextChunkFromWebp(files[0].TryGetLocalPath(), ExifTag.Model); + + return; + var metadata = ImageMetadata.ParseFile(files[0].TryGetLocalPath()!); var textualTags = metadata.GetTextualData()?.ToArray(); diff --git a/StabilityMatrix.Avalonia/Views/MainWindow.axaml.cs b/StabilityMatrix.Avalonia/Views/MainWindow.axaml.cs index c1263426..bd62a302 100644 --- a/StabilityMatrix.Avalonia/Views/MainWindow.axaml.cs +++ b/StabilityMatrix.Avalonia/Views/MainWindow.axaml.cs @@ -4,6 +4,9 @@ using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; +using System.Reactive; +using System.Reactive.Linq; +using System.Threading; using AsyncImageLoader; using Avalonia; using Avalonia.Controls; @@ -34,8 +37,11 @@ using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; +using StabilityMatrix.Core.Models.Settings; using StabilityMatrix.Core.Models.Update; using StabilityMatrix.Core.Processes; +using StabilityMatrix.Core.Services; +using TeachingTip = FluentAvalonia.UI.Controls.TeachingTip; #if DEBUG using StabilityMatrix.Avalonia.Diagnostics.Views; #endif @@ -48,6 +54,7 @@ public partial class MainWindow : AppWindowBase { private readonly INotificationService notificationService; private readonly INavigationService navigationService; + private readonly ISettingsManager settingsManager; private FlyoutBase? progressFlyout; @@ -61,11 +68,13 @@ public partial class MainWindow : AppWindowBase public MainWindow( INotificationService notificationService, - INavigationService navigationService + INavigationService navigationService, + ISettingsManager settingsManager ) { this.notificationService = notificationService; this.navigationService = navigationService; + this.settingsManager = settingsManager; InitializeComponent(); @@ -82,6 +91,47 @@ public partial class MainWindow : AppWindowBase EventManager.Instance.ToggleProgressFlyout += (_, _) => progressFlyout?.Hide(); EventManager.Instance.CultureChanged += (_, _) => SetDefaultFonts(); EventManager.Instance.UpdateAvailable += OnUpdateAvailable; + + Observable + .FromEventPattern(this, nameof(SizeChanged)) + .Where(x => x.EventArgs.PreviousSize != x.EventArgs.NewSize) + .Throttle(TimeSpan.FromMilliseconds(100)) + .Select(x => x.EventArgs.NewSize) + .ObserveOn(SynchronizationContext.Current) + .Subscribe(newSize => + { + var validWindowPosition = Screens.All.Any(screen => screen.Bounds.Contains(Position)); + + settingsManager.Transaction( + s => + { + s.WindowSettings = new WindowSettings( + newSize.Width, + newSize.Height, + validWindowPosition ? Position.X : 0, + validWindowPosition ? Position.Y : 0 + ); + }, + ignoreMissingLibraryDir: true + ); + }); + + Observable + .FromEventPattern(this, nameof(PositionChanged)) + .Where(x => Screens.All.Any(screen => screen.Bounds.Contains(x.EventArgs.Point))) + .Throttle(TimeSpan.FromMilliseconds(100)) + .Select(x => x.EventArgs.Point) + .ObserveOn(SynchronizationContext.Current) + .Subscribe(position => + { + settingsManager.Transaction( + s => + { + s.WindowSettings = new WindowSettings(Width, Height, position.X, position.Y); + }, + ignoreMissingLibraryDir: true + ); + }); } /// @@ -132,15 +182,16 @@ public partial class MainWindow : AppWindowBase return; // Navigate to first page - Dispatcher - .UIThread - .Post( - () => - navigationService.NavigateTo( - vm.Pages[0], - new BetterSlideNavigationTransition { Effect = SlideNavigationTransitionEffect.FromBottom } - ) - ); + Dispatcher.UIThread.Post( + () => + navigationService.NavigateTo( + vm.Pages[0], + new BetterSlideNavigationTransition + { + Effect = SlideNavigationTransitionEffect.FromBottom + } + ) + ); // Check show update teaching tip if (vm.UpdateViewModel.IsUpdateAvailable) @@ -165,27 +216,24 @@ public partial class MainWindow : AppWindowBase var mainViewModel = (MainWindowViewModel)DataContext!; mainViewModel.SelectedCategory = mainViewModel - .Pages - .Concat(mainViewModel.FooterPages) + .Pages.Concat(mainViewModel.FooterPages) .FirstOrDefault(x => x.GetType() == e.ViewModelType); } private void OnUpdateAvailable(object? sender, UpdateInfo? updateInfo) { - Dispatcher - .UIThread - .Post(() => + Dispatcher.UIThread.Post(() => + { + if (DataContext is MainWindowViewModel vm && vm.ShouldShowUpdateAvailableTeachingTip(updateInfo)) { - if (DataContext is MainWindowViewModel vm && vm.ShouldShowUpdateAvailableTeachingTip(updateInfo)) - { - var target = this.FindControl("FooterUpdateItem")!; - var tip = this.FindControl("UpdateAvailableTeachingTip")!; + var target = this.FindControl("FooterUpdateItem")!; + var tip = this.FindControl("UpdateAvailableTeachingTip")!; - tip.Target = target; - tip.Subtitle = $"{Compat.AppVersion.ToDisplayString()} -> {updateInfo.Version}"; - tip.IsOpen = true; - } - }); + tip.Target = target; + tip.Subtitle = $"{Compat.AppVersion.ToDisplayString()} -> {updateInfo.Version}"; + tip.IsOpen = true; + } + }); } public void SetDefaultFonts() @@ -282,18 +330,16 @@ public partial class MainWindow : AppWindowBase private void OnImageLoadFailed(object? sender, ImageLoadFailedEventArgs e) { - Dispatcher - .UIThread - .Post(() => - { - var fileName = Path.GetFileName(e.Url); - var displayName = string.IsNullOrEmpty(fileName) ? e.Url : fileName; - notificationService.ShowPersistent( - "Failed to load image", - $"Could not load '{displayName}'\n({e.Exception.Message})", - NotificationType.Warning - ); - }); + Dispatcher.UIThread.Post(() => + { + var fileName = Path.GetFileName(e.Url); + var displayName = string.IsNullOrEmpty(fileName) ? e.Url : fileName; + notificationService.ShowPersistent( + "Failed to load image", + $"Could not load '{displayName}'\n({e.Exception.Message})", + NotificationType.Warning + ); + }); } private void TryEnableMicaEffect() @@ -319,7 +365,11 @@ public partial class MainWindow : AppWindowBase else if (ActualThemeVariant == ThemeVariant.Light) { // Similar effect here - var color = this.TryFindResource("SolidBackgroundFillColorBase", ThemeVariant.Light, out var value) + var color = this.TryFindResource( + "SolidBackgroundFillColorBase", + ThemeVariant.Light, + out var value + ) ? (Color2)(Color)value! : new Color2(243, 243, 243); diff --git a/StabilityMatrix.Core/Helper/ImageMetadata.cs b/StabilityMatrix.Core/Helper/ImageMetadata.cs index a10dae53..aca320e7 100644 --- a/StabilityMatrix.Core/Helper/ImageMetadata.cs +++ b/StabilityMatrix.Core/Helper/ImageMetadata.cs @@ -1,7 +1,10 @@ -using System.Text; +using System.Diagnostics; +using System.Text; using System.Text.Json; +using ExifLibrary; using MetadataExtractor; using MetadataExtractor.Formats.Png; +using Microsoft.VisualBasic; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.FileInterfaces; @@ -13,10 +16,13 @@ public class ImageMetadata { private IReadOnlyList? Directories { get; set; } - private static readonly byte[] PngHeader = { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }; + private static readonly byte[] PngHeader = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]; private static readonly byte[] Idat = "IDAT"u8.ToArray(); private static readonly byte[] Text = "tEXt"u8.ToArray(); + private static readonly byte[] Riff = "RIFF"u8.ToArray(); + private static readonly byte[] Webp = "WEBP"u8.ToArray(); + public static ImageMetadata ParseFile(FilePath path) { return new ImageMetadata { Directories = ImageMetadataReader.ReadMetadata(path) }; @@ -179,4 +185,116 @@ public class ImageMetadata return string.Empty; } + + public static IEnumerable BuildImageWithoutMetadata(BinaryReader byteStream) + { + var bytes = new List(); + byteStream.BaseStream.Position = 0; + + // Read first 8 bytes and make sure they match the png header + if (!byteStream.ReadBytes(8).SequenceEqual(PngHeader)) + { + return Array.Empty(); + } + bytes.AddRange(PngHeader); + + var ihdrStuff = byteStream.ReadBytes(25); + bytes.AddRange(ihdrStuff); + + while (byteStream.BaseStream.Position < byteStream.BaseStream.Length - 4) + { + var chunkSizeBytes = byteStream.ReadBytes(4); + var chunkSize = BitConverter.ToInt32(chunkSizeBytes.Reverse().ToArray()); + var chunkTypeBytes = byteStream.ReadBytes(4); + var chunkType = Encoding.UTF8.GetString(chunkTypeBytes); + + if (chunkType != Encoding.UTF8.GetString(Idat)) + { + // skip chunk data + byteStream.BaseStream.Position += chunkSize; + // skip crc + byteStream.BaseStream.Position += 4; + continue; + } + + bytes.AddRange(chunkSizeBytes); + bytes.AddRange(chunkTypeBytes); + var idatBytes = byteStream.ReadBytes(chunkSize); + bytes.AddRange(idatBytes); + var crcBytes = byteStream.ReadBytes(4); + bytes.AddRange(crcBytes); + } + + // Add IEND chunk + bytes.AddRange([0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82]); + + return bytes; + } + + public static async Task ReadTextChunkFromWebp(FilePath filePath, ExifTag exifTag) + { + var sw = Stopwatch.StartNew(); + try + { + await using var memoryStream = Utilities.GetMemoryStreamFromFile(filePath); + if (memoryStream is null) + return string.Empty; + + var exifChunks = GetExifChunks(memoryStream); + if (exifChunks.Length == 0) + return string.Empty; + + // write exifChunks to new memoryStream but skip first 6 bytes + using var newMemoryStream = new MemoryStream(exifChunks[6..]); + newMemoryStream.Seek(0, SeekOrigin.Begin); + + var img = new MyTiffFile(newMemoryStream, Encoding.UTF8); + return img.Properties[exifTag]?.Value?.ToString() ?? string.Empty; + } + finally + { + sw.Stop(); + Console.WriteLine($"ReadTextChunkFromWebp took {sw.ElapsedMilliseconds}ms"); + } + } + + private static byte[] GetExifChunks(MemoryStream memoryStream) + { + using var byteStream = new BinaryReader(memoryStream); + byteStream.BaseStream.Position = 0; + + // Read first 8 bytes and make sure they match the RIFF header + if (!byteStream.ReadBytes(4).SequenceEqual(Riff)) + { + return Array.Empty(); + } + + // skip 4 bytes then read next 4 for webp header + byteStream.BaseStream.Position += 4; + if (!byteStream.ReadBytes(4).SequenceEqual(Webp)) + { + return Array.Empty(); + } + + while (byteStream.BaseStream.Position < byteStream.BaseStream.Length - 4) + { + var chunkType = Encoding.UTF8.GetString(byteStream.ReadBytes(4)); + var chunkSize = BitConverter.ToInt32(byteStream.ReadBytes(4).ToArray()); + + if (chunkType != "EXIF") + { + // skip chunk data + byteStream.BaseStream.Position += chunkSize; + continue; + } + + var exifStart = byteStream.BaseStream.Position; + var exifBytes = byteStream.ReadBytes(chunkSize); + var exif = Encoding.UTF8.GetString(exifBytes); + Debug.WriteLine($"Found exif chunk of size {chunkSize}"); + return exifBytes; + } + + return Array.Empty(); + } } diff --git a/StabilityMatrix.Core/Helper/MyTiffFile.cs b/StabilityMatrix.Core/Helper/MyTiffFile.cs new file mode 100644 index 00000000..dd65110b --- /dev/null +++ b/StabilityMatrix.Core/Helper/MyTiffFile.cs @@ -0,0 +1,6 @@ +using System.Text; +using ExifLibrary; + +namespace StabilityMatrix.Core.Helper; + +public class MyTiffFile(MemoryStream stream, Encoding encoding) : TIFFFile(stream, encoding); diff --git a/StabilityMatrix.Core/Helper/Utilities.cs b/StabilityMatrix.Core/Helper/Utilities.cs index 07afca73..5e8bfc2e 100644 --- a/StabilityMatrix.Core/Helper/Utilities.cs +++ b/StabilityMatrix.Core/Helper/Utilities.cs @@ -13,8 +13,12 @@ public static class Utilities : $"{version.Major}.{version.Minor}.{version.Build}.{version.Revision}"; } - public static void CopyDirectory(string sourceDir, string destinationDir, bool recursive, - bool includeReparsePoints = false) + public static void CopyDirectory( + string sourceDir, + string destinationDir, + bool recursive, + bool includeReparsePoints = false + ) { // Get information about the source directory var dir = new DirectoryInfo(sourceDir); @@ -35,11 +39,13 @@ public static class Utilities foreach (var file in dir.GetFiles()) { var targetFilePath = Path.Combine(destinationDir, file.Name); - if (file.FullName == targetFilePath) continue; + if (file.FullName == targetFilePath) + continue; file.CopyTo(targetFilePath, true); } - if (!recursive) return; + if (!recursive) + return; // If recursive and copying subdirectories, recursively call this method foreach (var subDir in dirs) @@ -48,4 +54,13 @@ public static class Utilities CopyDirectory(subDir.FullName, newDestinationDir, true); } } + + public static MemoryStream? GetMemoryStreamFromFile(string filePath) + { + var fileBytes = File.ReadAllBytes(filePath); + var stream = new MemoryStream(fileBytes); + stream.Position = 0; + + return stream; + } } diff --git a/StabilityMatrix.Core/Services/SettingsManager.cs b/StabilityMatrix.Core/Services/SettingsManager.cs index 0c0c56fa..2b0849c3 100644 --- a/StabilityMatrix.Core/Services/SettingsManager.cs +++ b/StabilityMatrix.Core/Services/SettingsManager.cs @@ -627,6 +627,7 @@ public class SettingsManager : ISettingsManager if (fileStream.Length == 0) { Logger.Warn("Settings file is empty, using default settings"); + isLoaded = true; return; } @@ -674,7 +675,18 @@ public class SettingsManager : ISettingsManager SettingsSerializerContext.Default.Settings ); - File.WriteAllBytes(SettingsPath, jsonBytes); + if (jsonBytes.Length == 0) + { + Logger.Error("JsonSerializer returned empty bytes for some reason"); + return; + } + + using var fs = File.Open(SettingsPath, FileMode.Open); + if (fs.CanWrite) + { + fs.Write(jsonBytes, 0, jsonBytes.Length); + } + fs.Close(); } finally { diff --git a/StabilityMatrix.Core/StabilityMatrix.Core.csproj b/StabilityMatrix.Core/StabilityMatrix.Core.csproj index d64996a3..e2304c75 100644 --- a/StabilityMatrix.Core/StabilityMatrix.Core.csproj +++ b/StabilityMatrix.Core/StabilityMatrix.Core.csproj @@ -27,6 +27,7 @@ +