diff options
| author | Dennis Brentjes <d.brentjes@gmail.com> | 2020-01-01 22:50:08 +0100 |
|---|---|---|
| committer | Dennis Brentjes <d.brentjes@gmail.com> | 2020-01-01 22:50:08 +0100 |
| commit | 2481d1857b08879a3d15e679a302667c7d8976f7 (patch) | |
| tree | c8ae66b72a3469e65239c94d1d52795fb75700b2 | |
| parent | fbe866f7b26c10bb54d72c029f8c628988a90be2 (diff) | |
| download | avalar-2481d1857b08879a3d15e679a302667c7d8976f7.tar.gz avalar-2481d1857b08879a3d15e679a302667c7d8976f7.tar.bz2 avalar-2481d1857b08879a3d15e679a302667c7d8976f7.zip | |
Adds a working but slightly buggy pipeline implementation.
| -rw-r--r-- | MainWindow.xaml.cs | 10 | ||||
| -rw-r--r-- | Models/AvalarPipeline.cs | 47 | ||||
| -rw-r--r-- | Models/IPipelineStep.cs | 13 | ||||
| -rw-r--r-- | Models/ImageModel.cs | 69 | ||||
| -rw-r--r-- | Models/Interfaces/IImageModel.cs | 5 | ||||
| -rw-r--r-- | Models/Interfaces/ISettingsModel.cs | 14 | ||||
| -rw-r--r-- | Models/Interfaces/SettingsModel.cs | 12 | ||||
| -rw-r--r-- | Models/PipelineStep.cs | 65 | ||||
| -rw-r--r-- | Services/IProcessor.cs | 6 | ||||
| -rw-r--r-- | Services/ImageMagickBrightness.cs | 25 | ||||
| -rw-r--r-- | Services/ImageMagickResizer.cs | 34 | ||||
| -rw-r--r-- | Services/Interfaces/IBrightness.cs | 8 | ||||
| -rw-r--r-- | Services/Interfaces/IResizer.cs | 9 | ||||
| -rw-r--r-- | Services/Resizer/IResizer.cs | 7 | ||||
| -rw-r--r-- | Services/Resizer/ImageMagickResizer.cs | 26 | ||||
| -rw-r--r-- | Utils/BitmapExtensions.cs | 25 | ||||
| -rw-r--r-- | Utils/ServiceProviderExtensions.cs | 13 | ||||
| -rw-r--r-- | ViewModels/Image/ImageLoadedViewModel.cs | 26 | ||||
| -rw-r--r-- | ViewModels/Image/ImageViewModel.cs | 10 | ||||
| -rw-r--r-- | ViewModels/Settings/ISettingLimits.cs | 9 | ||||
| -rw-r--r-- | ViewModels/Settings/SettingLimits.cs | 20 | ||||
| -rw-r--r-- | ViewModels/Settings/SettingsViewModel.cs | 52 | ||||
| -rw-r--r-- | Views/Settings/Settings.xaml | 8 |
23 files changed, 437 insertions, 76 deletions
diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index b2a5090..1ddc1a6 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -1,5 +1,6 @@ using Avalar.Models; -using Avalar.Services.Resizer; +using Avalar.Services; +using Avalar.Services.Interfaces; using Avalar.Viewmodels.Image; using Avalar.ViewModels.Settings; using Avalar.Views; @@ -20,15 +21,18 @@ namespace Avalar { InitializeComponent(); var serviceCollection = new ServiceCollection(); + serviceCollection.AddScoped(typeof(IResizer), typeof(ImageMagickResizer)); + serviceCollection.AddScoped(typeof(IBrightness), typeof(ImageMagickBrightness)); + serviceCollection.AddScoped(typeof(IAvalarViewService), (sp) => new AvalarViewService(this)); var serviceProvider = serviceCollection.BuildServiceProvider(); - ImageM = new ImageModel(); + ImageM = new ImageModel(serviceProvider); ImageVm = new ImageViewModel(ImageM, serviceProvider); - SettingsVm = new SettingsViewModel(); + SettingsVm = new SettingsViewModel(ImageM); DataContext = this; } diff --git a/Models/AvalarPipeline.cs b/Models/AvalarPipeline.cs new file mode 100644 index 0000000..14e32d0 --- /dev/null +++ b/Models/AvalarPipeline.cs @@ -0,0 +1,47 @@ +using Avalonia.Media.Imaging; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Avalar.Models +{ + public class AvalarPipeline + { + private readonly IEnumerable<IPipelineStep> m_steps; + + public AvalarPipeline(IEnumerable<IPipelineStep> steps) + { + m_steps = steps ?? throw new ArgumentNullException(nameof(steps)); + foreach (var step in m_steps) + { + step.PropertyChanged += OnPipelineComponentChanged; + } + } + + private void OnPipelineComponentChanged(object sender, PropertyChangedEventArgs e) + { + foreach (var step in m_steps.SkipWhile(step => step != sender)) + { + step.InvalidateResult(); + } + + PipelineChanged?.Invoke(this, EventArgs.Empty); + } + + public event EventHandler PipelineChanged; + + public async Task<IBitmap> Run(IBitmap bitmap, CancellationToken token) + { + var input = bitmap; + foreach(var step in m_steps) + { + token.ThrowIfCancellationRequested(); + input = await step.Run(input, token).ConfigureAwait(false); + } + return input; + } + } +} diff --git a/Models/IPipelineStep.cs b/Models/IPipelineStep.cs new file mode 100644 index 0000000..5f53c42 --- /dev/null +++ b/Models/IPipelineStep.cs @@ -0,0 +1,13 @@ +using Avalonia.Media.Imaging; +using System.ComponentModel; +using System.Threading.Tasks; + +namespace Avalar.Models +{ + public interface IPipelineStep : INotifyPropertyChanged + { + public Task<IBitmap> Run(IBitmap bitmap, System.Threading.CancellationToken token); + + public void InvalidateResult(); + } +}
\ No newline at end of file diff --git a/Models/ImageModel.cs b/Models/ImageModel.cs index e0bb1af..047d2e0 100644 --- a/Models/ImageModel.cs +++ b/Models/ImageModel.cs @@ -1,28 +1,61 @@ using Avalar.Models.Interfaces; +using Avalar.Utils; +using Avalar.Services.Interfaces; using Avalonia.Media.Imaging; using ReactiveUI; using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; namespace Avalar.Models { public class ImageModel : ReactiveObject, IImageModel, ISettingsModel, IDisposable { - + private readonly IResizer m_Resizer; + private readonly IBrightness m_Brightness; + private CancellationTokenSource m_CancellationTokenSource; + private AvalarPipeline m_pipeline; private IBitmap m_OriginalBitmap; - private IBitmap m_LatestResult; + public ImageModel(IServiceProvider serviceProvider) + { + m_Resizer = serviceProvider.GetService<IResizer>(); + m_Brightness = serviceProvider.GetService<IBrightness>(); - private double m_Width; - public double Width { - get => m_Width; - set => this.RaiseAndSetIfChanged(ref m_Width, value); + m_pipeline = new AvalarPipeline(new List<IPipelineStep> + { + m_Resizer.AsPipelineStep(), + m_Brightness.AsPipelineStep() + }); + + m_pipeline.PipelineChanged += OnPipelineChanged; } - private double m_Height; - public double Height + public async Task RunPipeline() { - get => m_Height; - set => this.RaiseAndSetIfChanged(ref m_Height, value); + try + { + LatestResult = await m_pipeline.Run(m_OriginalBitmap, m_CancellationTokenSource.Token).ConfigureAwait(false); + } catch (OperationCanceledException) + { + + } + } + + private async void OnPipelineChanged(object sender, EventArgs e) + { + m_CancellationTokenSource?.Cancel(); + m_CancellationTokenSource = new CancellationTokenSource(); + + await RunPipeline().ConfigureAwait(false); + } + + private IBitmap m_LatestResult; + public IBitmap LatestResult + { + get => m_LatestResult; + set => this.RaiseAndSetIfChanged(ref m_LatestResult, value); } public IBitmap LoadImage(string imagePath) @@ -48,9 +81,25 @@ namespace Avalar.Models { m_OriginalBitmap.Dispose(); m_LatestResult.Dispose(); + m_CancellationTokenSource.Dispose(); } disposed = true; } + + public void SetWidth(uint width) + { + m_Resizer.Width = width; + } + + public void SetHeight(uint height) + { + m_Resizer.Height = height; + } + + public void SetBrightnessDelta(int brightnessDelta) + { + m_Brightness.Brightness = brightnessDelta; + } } } diff --git a/Models/Interfaces/IImageModel.cs b/Models/Interfaces/IImageModel.cs index 897a221..d8f5870 100644 --- a/Models/Interfaces/IImageModel.cs +++ b/Models/Interfaces/IImageModel.cs @@ -1,9 +1,12 @@ using Avalonia.Media.Imaging; +using System.ComponentModel; namespace Avalar.Models.Interfaces { - public interface IImageModel + public interface IImageModel : INotifyPropertyChanged { IBitmap LoadImage(string imagePath); + + IBitmap LatestResult { get; } } } diff --git a/Models/Interfaces/ISettingsModel.cs b/Models/Interfaces/ISettingsModel.cs new file mode 100644 index 0000000..acdb187 --- /dev/null +++ b/Models/Interfaces/ISettingsModel.cs @@ -0,0 +1,14 @@ + +using Avalonia; + +namespace Avalar.Models.Interfaces +{ + public interface ISettingsModel + { + public void SetWidth(uint width); + + public void SetHeight(uint height); + + public void SetBrightnessDelta(int brightnessDelta); + } +} diff --git a/Models/Interfaces/SettingsModel.cs b/Models/Interfaces/SettingsModel.cs deleted file mode 100644 index 6149c1e..0000000 --- a/Models/Interfaces/SettingsModel.cs +++ /dev/null @@ -1,12 +0,0 @@ - -using Avalonia; - -namespace Avalar.Models.Interfaces -{ - interface ISettingsModel - { - public double Width { get; set; } - - public double Height { get; set; } - } -} diff --git a/Models/PipelineStep.cs b/Models/PipelineStep.cs new file mode 100644 index 0000000..e8a4bf1 --- /dev/null +++ b/Models/PipelineStep.cs @@ -0,0 +1,65 @@ +using Avalar.Services; +using Avalonia.Media.Imaging; +using ReactiveUI; +using System; +using System.ComponentModel; +using System.Threading; +using System.Threading.Tasks; + +namespace Avalar.Models +{ + public class PipelineStep<T> : IPipelineStep where T : INotifyPropertyChanged, IProcessor + { + public PipelineStep(T processor) + { + m_Processor = processor ?? throw new ArgumentNullException(nameof(processor)); + m_Processor.PropertyChanged += OnPropertyChanged; + } + + private void OnPropertyChanged(object sender, PropertyChangedEventArgs e) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Result))); + } + + public void InvalidateResult() + { + Result = null; + } + + private T m_Processor; + + public event PropertyChangedEventHandler PropertyChanged; + + private IBitmap Result { get; set; } + + public Task<IBitmap> Run(IBitmap bitmap, CancellationToken token) + { + if (Result != null) + { + return Task.FromResult(Result); + } + else + { + return Task.Run(() => + { + Result = m_Processor.Process(bitmap, token); + token.ThrowIfCancellationRequested(); + return Result; + }); + } + } + } + + public static class PipelineStep + { + public static PipelineStep<T> Create<T>(T processor) where T : INotifyPropertyChanged, IProcessor + { + return new PipelineStep<T>(processor); + } + + public static PipelineStep<T> AsPipelineStep<T>(this T processor) where T : INotifyPropertyChanged, IProcessor + { + return Create(processor); + } + } +} diff --git a/Services/IProcessor.cs b/Services/IProcessor.cs index 11e83ab..00bd6a8 100644 --- a/Services/IProcessor.cs +++ b/Services/IProcessor.cs @@ -1,9 +1,11 @@ using Avalonia.Media.Imaging; +using System.ComponentModel; +using System.Threading.Tasks; namespace Avalar.Services { - public interface IProcessor + public interface IProcessor : INotifyPropertyChanged { - IBitmap Process(IBitmap bitmap); + public IBitmap Process(IBitmap input, System.Threading.CancellationToken token); } } diff --git a/Services/ImageMagickBrightness.cs b/Services/ImageMagickBrightness.cs new file mode 100644 index 0000000..f69247e --- /dev/null +++ b/Services/ImageMagickBrightness.cs @@ -0,0 +1,25 @@ +using Avalar.Services.Interfaces; +using Avalonia.Media.Imaging; +using ImageMagick; +using ReactiveUI; +using System.IO; +using System.Threading; + +namespace Avalar.Services +{ + public class ImageMagickBrightness : ReactiveObject, IBrightness + { + int m_Brightness; + public int Brightness { + get => m_Brightness; + set => this.RaiseAndSetIfChanged(ref m_Brightness, value); + } + + public IBitmap Process(IBitmap input, CancellationToken token) + { + using var image = input.ToMagickImage(); + image.BrightnessContrast(new Percentage(Brightness), new Percentage(0)); + return image.ToBitmap(); + } + } +} diff --git a/Services/ImageMagickResizer.cs b/Services/ImageMagickResizer.cs new file mode 100644 index 0000000..b7ce6da --- /dev/null +++ b/Services/ImageMagickResizer.cs @@ -0,0 +1,34 @@ + +using Avalar.Services.Interfaces; +using Avalonia.Media.Imaging; +using ImageMagick; +using ReactiveUI; +using System; +using System.Threading; + +namespace Avalar.Services +{ + public class ImageMagickResizer : ReactiveObject, IResizer + { + private uint m_Width; + public uint Width { + get => m_Width; + set => this.RaiseAndSetIfChanged(ref m_Width, value); + } + + private uint m_Height; + public uint Height { + get => m_Height; + set => this.RaiseAndSetIfChanged(ref m_Height, value); + } + + public IBitmap Process(IBitmap input, CancellationToken token) + { + using var image = input.ToMagickImage(); + var geometry = new MagickGeometry(Convert.ToInt32(Width), Convert.ToInt32(Height)); + geometry.IgnoreAspectRatio = true; + image.Resize(geometry); + return image.ToBitmap(); + } + } +} diff --git a/Services/Interfaces/IBrightness.cs b/Services/Interfaces/IBrightness.cs new file mode 100644 index 0000000..cf8f631 --- /dev/null +++ b/Services/Interfaces/IBrightness.cs @@ -0,0 +1,8 @@ + +namespace Avalar.Services.Interfaces +{ + interface IBrightness : IProcessor + { + public int Brightness { get; set; } + } +} diff --git a/Services/Interfaces/IResizer.cs b/Services/Interfaces/IResizer.cs new file mode 100644 index 0000000..d7e5cdd --- /dev/null +++ b/Services/Interfaces/IResizer.cs @@ -0,0 +1,9 @@ + +namespace Avalar.Services.Interfaces +{ + public interface IResizer : IProcessor + { + public uint Width { get; set; } + public uint Height { get; set; } + } +} diff --git a/Services/Resizer/IResizer.cs b/Services/Resizer/IResizer.cs deleted file mode 100644 index e71d4f1..0000000 --- a/Services/Resizer/IResizer.cs +++ /dev/null @@ -1,7 +0,0 @@ - -namespace Avalar.Services.Resizer -{ - public interface IResizer : IProcessor - { - } -} diff --git a/Services/Resizer/ImageMagickResizer.cs b/Services/Resizer/ImageMagickResizer.cs deleted file mode 100644 index e48e576..0000000 --- a/Services/Resizer/ImageMagickResizer.cs +++ /dev/null @@ -1,26 +0,0 @@ - -using Avalonia.Media.Imaging; -using ImageMagick; -using System.IO; - -namespace Avalar.Services.Resizer -{ - public class ImageMagickResizer : IResizer - { - public IBitmap Process(IBitmap bitmap) - { - using (var stream = new MemoryStream()) - { - bitmap?.Save(stream); - using (var image = new MagickImage(stream)) - { - image.Resize(300, 300); - using (var stream2 = new MemoryStream()) - { - return new Bitmap(stream2); - } - } - } - } - } -} diff --git a/Utils/BitmapExtensions.cs b/Utils/BitmapExtensions.cs new file mode 100644 index 0000000..89f0257 --- /dev/null +++ b/Utils/BitmapExtensions.cs @@ -0,0 +1,25 @@ +using Avalonia.Media.Imaging; +using ImageMagick; +using System.IO; + +namespace Avalar.Services +{ + public static class BitmapExtensions + { + public static MagickImage ToMagickImage(this IBitmap bitmap) + { + using var stream = new MemoryStream(); + bitmap?.Save(stream); + stream.Seek(0, SeekOrigin.Begin); + return new MagickImage(stream); + } + + public static IBitmap ToBitmap(this MagickImage image) + { + using var stream = new MemoryStream(); + image?.Write(stream, MagickFormat.Png); + stream.Seek(0, SeekOrigin.Begin); + return new Bitmap(stream); + } + } +} diff --git a/Utils/ServiceProviderExtensions.cs b/Utils/ServiceProviderExtensions.cs new file mode 100644 index 0000000..1ed41f0 --- /dev/null +++ b/Utils/ServiceProviderExtensions.cs @@ -0,0 +1,13 @@ + +using System; + +namespace Avalar.Utils +{ + public static class ServiceProviderExtensions + { + public static T GetService<T>(this IServiceProvider serviceProvider) + { + return (T)serviceProvider?.GetService(typeof(T)); + } + } +} diff --git a/ViewModels/Image/ImageLoadedViewModel.cs b/ViewModels/Image/ImageLoadedViewModel.cs index f9e322c..0489ac1 100644 --- a/ViewModels/Image/ImageLoadedViewModel.cs +++ b/ViewModels/Image/ImageLoadedViewModel.cs @@ -1,23 +1,37 @@ -using Avalonia.Media.Imaging; +using Avalar.Models.Interfaces; +using Avalonia.Media.Imaging; +using Avalonia.Threading; using ReactiveUI; +using System; +using System.ComponentModel; namespace Avalar.ViewModels.Image { public class ImageLoadedViewModel : ReactiveObject { - public ImageLoadedViewModel(IBitmap bitmap) + private IImageModel m_ImageModel; + + public ImageLoadedViewModel(IBitmap bitmap, IImageModel imageModel) { Bitmap = bitmap; + m_ImageModel = imageModel ?? throw new ArgumentNullException(nameof(imageModel)); + + m_ImageModel.PropertyChanged += OnImageModelChanged; + } + + private void OnImageModelChanged(object sender, PropertyChangedEventArgs e) + { + Dispatcher.UIThread.InvokeAsync(() => + { + Bitmap = m_ImageModel.LatestResult; + }); } private IBitmap m_Bitmap; public IBitmap Bitmap { get => m_Bitmap; - set - { - this.RaiseAndSetIfChanged(ref m_Bitmap, value); - } + set => this.RaiseAndSetIfChanged(ref m_Bitmap, value); } } } diff --git a/ViewModels/Image/ImageViewModel.cs b/ViewModels/Image/ImageViewModel.cs index 3b41919..4b17343 100644 --- a/ViewModels/Image/ImageViewModel.cs +++ b/ViewModels/Image/ImageViewModel.cs @@ -1,8 +1,6 @@ - -using Avalar.Models.Interfaces; +using Avalar.Models.Interfaces; using Avalar.ViewModels.Image; using Avalar.Views; -using Avalonia.Media.Imaging; using ReactiveUI; using System; using System.Linq; @@ -27,9 +25,7 @@ namespace Avalar.Viewmodels.Image public object ChildViewModel { get => m_ChildViewModel; - set { - this.RaiseAndSetIfChanged(ref m_ChildViewModel, value); - } + set => this.RaiseAndSetIfChanged(ref m_ChildViewModel, value); } public async void OpenFile() @@ -37,7 +33,7 @@ namespace Avalar.Viewmodels.Image var result = await ViewService.ShowOpenImageFileDialog().ConfigureAwait(true); if (result.Length != 1) return; - ChildViewModel = new ImageLoadedViewModel(ImageModel.LoadImage(result.First())); + ChildViewModel = new ImageLoadedViewModel(ImageModel.LoadImage(result.First()), ImageModel); } } } diff --git a/ViewModels/Settings/ISettingLimits.cs b/ViewModels/Settings/ISettingLimits.cs new file mode 100644 index 0000000..a4927f0 --- /dev/null +++ b/ViewModels/Settings/ISettingLimits.cs @@ -0,0 +1,9 @@ +namespace Avalar.ViewModels.Settings +{ + public interface ISettingLimits + { + uint MaxWidth { get; } + + uint MaxHeight { get; } + } +}
\ No newline at end of file diff --git a/ViewModels/Settings/SettingLimits.cs b/ViewModels/Settings/SettingLimits.cs new file mode 100644 index 0000000..8a0f3ea --- /dev/null +++ b/ViewModels/Settings/SettingLimits.cs @@ -0,0 +1,20 @@ +using Avalonia.Media.Imaging; +using System; + +namespace Avalar.ViewModels.Settings +{ + public class SettingLimits : ISettingLimits + { + public SettingLimits(IBitmap bitmap) + { + var _ = bitmap ?? throw new ArgumentNullException(nameof(bitmap)); + + MaxWidth = Convert.ToUInt32(bitmap.Size.Width); + MaxHeight = Convert.ToUInt32(bitmap.Size.Height); + } + + public uint MaxWidth { get; } + + public uint MaxHeight { get; } + } +} diff --git a/ViewModels/Settings/SettingsViewModel.cs b/ViewModels/Settings/SettingsViewModel.cs index 730b662..17c27cc 100644 --- a/ViewModels/Settings/SettingsViewModel.cs +++ b/ViewModels/Settings/SettingsViewModel.cs @@ -1,7 +1,57 @@ +using Avalar.Models.Interfaces; +using ReactiveUI; +using System; +using System.ComponentModel; +using System.Threading.Tasks; + namespace Avalar.ViewModels.Settings { - public class SettingsViewModel + public class SettingsViewModel : ReactiveObject { + private readonly ISettingsModel m_SettingsModel; + public SettingsViewModel(ISettingsModel settingsModel) + { + m_SettingsModel = settingsModel; + + PropertyChanged += OnPropertyChanged; + } + + private async void OnPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if(e.PropertyName == nameof(Width)) + { + await Task.Run(() => m_SettingsModel.SetWidth(Width)).ConfigureAwait(false); + } + if (e.PropertyName == nameof(Height)) + { + await Task.Run(() => m_SettingsModel.SetHeight(Height)).ConfigureAwait(false); + } + if (e.PropertyName == nameof(BrightnessDelta)) + { + await Task.Run(() => m_SettingsModel.SetBrightnessDelta(BrightnessDelta)).ConfigureAwait(false); + } + } + + private uint m_Width; + public uint Width + { + get => m_Width; + set => this.RaiseAndSetIfChanged(ref m_Width, value); + } + + private uint m_Height; + public uint Height + { + get => m_Height; + set => this.RaiseAndSetIfChanged(ref m_Height, value); + } + + private int m_BrightnessDelta; + public int BrightnessDelta + { + get => m_BrightnessDelta; + set => this.RaiseAndSetIfChanged(ref m_BrightnessDelta, value); + } } } diff --git a/Views/Settings/Settings.xaml b/Views/Settings/Settings.xaml index fb547e7..5a83bfa 100644 --- a/Views/Settings/Settings.xaml +++ b/Views/Settings/Settings.xaml @@ -5,5 +5,11 @@ mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Avalar.Views.Settings.SettingsControl" MinWidth="400"> - Welcome to Avalonia! + <Grid> + <StackPanel Orientation="Vertical"> + <Slider Value="{Binding Width}" Minimum="0" Maximum="2000"/> + <Slider Value="{Binding Height}" Minimum="0" Maximum="2000"/> + <Slider Value="{Binding BrightnessDelta}" Minimum="-100" Maximum="100"/> + </StackPanel> + </Grid> </UserControl> |
