Browse Source

handle window size/position outside of shutdown, load pngs faster, start of saving some img2vid stuff, webp metadata reading, and different settings write method (that still doesn't entirely work and occasionally adds extra right curly brackets to the end of the file)

pull/438/head
JT 11 months ago
parent
commit
d533be3329
  1. 224
      StabilityMatrix.Avalonia/App.axaml.cs
  2. 18
      StabilityMatrix.Avalonia/FallbackRamCachedWebImageLoader.cs
  3. 11
      StabilityMatrix.Avalonia/ViewModels/Inference/Video/SvdImgToVidConditioningViewModel.cs
  4. 11
      StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs
  5. 5
      StabilityMatrix.Avalonia/ViewModels/Settings/MainSettingsViewModel.cs
  6. 124
      StabilityMatrix.Avalonia/Views/MainWindow.axaml.cs
  7. 122
      StabilityMatrix.Core/Helper/ImageMetadata.cs
  8. 6
      StabilityMatrix.Core/Helper/MyTiffFile.cs
  9. 23
      StabilityMatrix.Core/Helper/Utilities.cs
  10. 14
      StabilityMatrix.Core/Services/SettingsManager.cs
  11. 1
      StabilityMatrix.Core/StabilityMatrix.Core.csproj

224
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<DataAnnotationsValidationPlugin>()
.DataValidators.OfType<DataAnnotationsValidationPlugin>()
.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<ViewModelBase>();
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<SingletonAttribute>().ToArray() });
.Select(
t1 => new { Type = t1.t, Attributes = t1.attributes.Cast<SingletonAttribute>().ToArray() }
);
foreach (var typePair in singletonTypes)
{
@ -386,7 +388,9 @@ public sealed class App : Application
// Rich presence
services.AddSingleton<IDiscordRichPresenceService, DiscordRichPresenceService>();
services.AddSingleton<IDisposable>(provider => provider.GetRequiredService<IDiscordRichPresenceService>());
services.AddSingleton<IDisposable>(
provider => provider.GetRequiredService<IDiscordRichPresenceService>()
);
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>();
launchPageViewModel.OnMainWindowClosing(e);
@ -602,66 +608,38 @@ public sealed class App : Application
// Check if we need to dispose IAsyncDisposables
if (
!isAsyncDisposeComplete
&& Services.GetServices<IAsyncDisposable>().ToList() is { Count: > 0 } asyncDisposables
isAsyncDisposeComplete
|| Services.GetServices<IAsyncDisposable>().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);
}
/// <summary>
/// Called at the end of <see cref="OnMainWindowClosing"/> before the main window is closed.
/// </summary>
private static void OnMainWindowClosingTerminal(Window sender)
{
var settingsManager = Services.GetRequiredService<ISettingsManager>();
// 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")]

18
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<Bitmap> 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);
}
}

11
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
}
);

11
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<OneClickInstallViewModel>();
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();

5
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();

124
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<MainWindowViewModel> navigationService;
private readonly ISettingsManager settingsManager;
private FlyoutBase? progressFlyout;
@ -61,11 +68,13 @@ public partial class MainWindow : AppWindowBase
public MainWindow(
INotificationService notificationService,
INavigationService<MainWindowViewModel> navigationService
INavigationService<MainWindowViewModel> 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<SizeChangedEventArgs>(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<PixelPointEventArgs>(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
);
});
}
/// <inheritdoc />
@ -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<NavigationViewItem>("FooterUpdateItem")!;
var tip = this.FindControl<TeachingTip>("UpdateAvailableTeachingTip")!;
var target = this.FindControl<NavigationViewItem>("FooterUpdateItem")!;
var tip = this.FindControl<TeachingTip>("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);

122
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<Directory>? 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<byte> BuildImageWithoutMetadata(BinaryReader byteStream)
{
var bytes = new List<byte>();
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<byte>();
}
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<string> 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<byte>();
}
// skip 4 bytes then read next 4 for webp header
byteStream.BaseStream.Position += 4;
if (!byteStream.ReadBytes(4).SequenceEqual(Webp))
{
return Array.Empty<byte>();
}
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<byte>();
}
}

6
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);

23
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;
}
}

14
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
{

1
StabilityMatrix.Core/StabilityMatrix.Core.csproj

@ -27,6 +27,7 @@
<PackageReference Include="DeviceId.Windows.Wmi" Version="6.5.1" />
<PackageReference Include="DotNet.Bundle" Version="0.9.13" />
<PackageReference Include="DynamicData" Version="8.1.1" />
<PackageReference Include="ExifLibNet" Version="2.1.4" />
<PackageReference Include="Hardware.Info" Version="100.0.0.1" />
<PackageReference Include="JetBrains.Annotations" Version="2023.3.0" />
<PackageReference Include="KGySoft.Drawing.Core" Version="8.0.0" />

Loading…
Cancel
Save