Skip to content

Commit

Permalink
Add image processing features and UI updates
Browse files Browse the repository at this point in the history
- Removed unnecessary target frameworks from project file.
- Added new ImageProcessing page with effects like blur, grayscale, etc.
- Implemented event handlers for effect adjustments in the UI.
- Introduced a collection to manage multiple image processing effects.
- Updated main page to navigate to the new ImageProcessing page.
michaelstonis committed Dec 4, 2024
1 parent 6166515 commit ee5e377
Showing 21 changed files with 1,696 additions and 125 deletions.
6 changes: 3 additions & 3 deletions AuroraControls.TestApp/AuroraControls.TestApp.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net8.0-android;net8.0-ios;net8.0-maccatalyst</TargetFrameworks>
<TargetFrameworks>net8.0-android;net8.0-ios;net8.0-maccatalyst</TargetFrameworks>
<OutputType>Exe</OutputType>
<RootNamespace>AuroraControls.TestApp</RootNamespace>
<UseMaui>true</UseMaui>
@@ -52,7 +52,7 @@
<PackageReference Include="ReactiveUI.Fody" Version="19.5.41" />
<PackageReference Include="CommunityToolkit.Maui.Markup" Version="4.1.0" />
<PackageReference Include="CommunityToolkit.Maui" Version="9.0.3" />
<PackageReference Include="Microsoft.Maui.Controls" Version="8.0.90" />
<PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="8.0.90" />
<PackageReference Include="Microsoft.Maui.Controls" Version="8.0.90" />
<PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="8.0.90" />
</ItemGroup>
</Project>
25 changes: 25 additions & 0 deletions AuroraControls.TestApp/ImageProcessing.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:effects="clr-namespace:AuroraControls.Effects;assembly=AuroraControls"
x:Class="AuroraControls.TestApp.ImageProcessing">
<ContentPage.Content>
<StackLayout HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand">
<Button Text="Change Effects"
Clicked="Handle_Clicked" />
<Label Text="Blur Amount" />
<Slider Minimum="0"
Maximum="10"
ValueChanged="Handle_ValueChanged" />
<Image x:Name="image"
Source="https://api.floodmagazine.com/wp-content/uploads/2016/07/Steve_Brule-2016-Marc_Lemoine-2.png"
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand">
<Image.Effects>
<effects:ImageProcessingEffect x:Name="ImageProcessingEffect" />
</Image.Effects>
</Image>
</StackLayout>
</ContentPage.Content>
</ContentPage>
61 changes: 61 additions & 0 deletions AuroraControls.TestApp/ImageProcessing.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace AuroraControls.TestApp;

public partial class ImageProcessing : ContentPage
{
private List<AuroraControls.ImageProcessing.ImageProcessingBase> _imageProcessing = new();

private int _index = 0;

private AuroraControls.ImageProcessing.Blur _blur;
private AuroraControls.ImageProcessing.Circular _circular;
private AuroraControls.ImageProcessing.Grayscale _grayscale;
private AuroraControls.ImageProcessing.Invert _invert;
private AuroraControls.ImageProcessing.Scale _scale;
private AuroraControls.ImageProcessing.Sepia _sepia;

private Random _rngesus = new Random(Guid.NewGuid().GetHashCode());

public ImageProcessing()
{
InitializeComponent();

_blur = new AuroraControls.ImageProcessing.Blur { };
_circular = new AuroraControls.ImageProcessing.Circular();
_grayscale = new AuroraControls.ImageProcessing.Grayscale();
_invert = new AuroraControls.ImageProcessing.Invert();
_scale = new AuroraControls.ImageProcessing.Scale();
_sepia = new AuroraControls.ImageProcessing.Sepia();

this._imageProcessing.AddRange([_blur, _circular, _grayscale, _invert, _scale, _sepia]);
}

private void Handle_ValueChanged(object sender, ValueChangedEventArgs e)
{
_blur.BlurAmount = e.NewValue;
}

private void Handle_Clicked(object sender, System.EventArgs e)
{
if (_index > _imageProcessing.Count - 1)
{
_index = 0;
}

var processingEffect = this._imageProcessing.ElementAt(_index);

if (ImageProcessingEffect.ImageProcessingEffects.Contains(processingEffect))
{
ImageProcessingEffect.ImageProcessingEffects.Remove(processingEffect);
}

ImageProcessingEffect.ImageProcessingEffects.Add(processingEffect);

_index++;
}
}
164 changes: 52 additions & 112 deletions AuroraControls.TestApp/MainPage.cs
Original file line number Diff line number Diff line change
@@ -50,6 +50,8 @@ public TestMvvmToolkitViewModel MvvmToolkitViewModel

private CupertinoTextToggleSwitch _cupertinoToggleSwitch;

private Button _viewImageProcessingButton;

public MainPage(ILogger<TestRxViewModel> logger)
{
var val = 123;
@@ -70,81 +72,52 @@ public MainPage(ILogger<TestRxViewModel> logger)
Spacing = 16,
Children =
{
new Label
{
HorizontalOptions = LayoutOptions.Center,
VerticalOptions = LayoutOptions.Center,
Text = "Welcome to .NET MAUI!",
},
new Button { Text = "View Image Processing", }
.Assign(out _viewImageProcessingButton),
new Grid
{
ColumnDefinitions = Columns.Define(Auto, Star, Auto),
RowDefinitions = Rows.Define(Auto),
Children =
{
new CupertinoTextToggleSwitch()
{
EnabledText = "Enabled",
DisabledText = "Disabled",
TrackDisabledColor = Color.FromRgba("#ef361a"),
TrackEnabledColor = Color.FromRgba("#4694f2"),
DisabledFontColor = Colors.White,
EnabledFontColor = Colors.White,
}
{
EnabledText = "Enabled",
DisabledText = "Disabled",
TrackDisabledColor = Color.FromRgba("#ef361a"),
TrackEnabledColor = Color.FromRgba("#4694f2"),
DisabledFontColor = Colors.White,
EnabledFontColor = Colors.White,
}
.Bind(CupertinoTextToggleSwitch.IsToggledProperty, nameof(TestRxViewModel.IsToggled), mode: BindingMode.TwoWay)
.TapGesture(() => _cupertinoToggleSwitch.EnabledText += "A")
.Row(0).Column(2)
.Assign(out _cupertinoToggleSwitch),
},
},
new Button
{
BackgroundColor = Colors.Fuchsia,
}
new Button { BackgroundColor = Colors.Fuchsia, }
.SetSvgIcon("splatoon.svg", colorOverride: Colors.White),
new SegmentedControl
{
FontFamily = "Clathing",
SegmentControlStyle = SegmentedControlStyle.Cupertino,
ForegroundTextColor = Colors.CadetBlue,
BackgroundTextColor = Colors.DarkSlateGray,
Segments =
{
new Segment
{
ForegroundColor = Colors.Lime,
Text = "Test 1",
},
new Segment
{
EmbeddedImageName = "splatoon.svg",
ForegroundColor = Colors.Fuchsia,
Text = "Test 2",
},
},
Segments = { new Segment { ForegroundColor = Colors.Lime, Text = "Test 1", }, new Segment { EmbeddedImageName = "splatoon.svg", ForegroundColor = Colors.Fuchsia, Text = "Test 2", }, },
},
new StyledInputLayout
{
Opacity = .25d,
BackgroundColor = Colors.Fuchsia,
ActiveColor = Colors.Red,
InactiveColor = Colors.Green,
PlaceholderColor = Colors.Purple,
BorderStyle = ContainerBorderStyle.RoundedRectanglePlaceholderThrough,
Content =
new Entry
{
Placeholder = "My Placeholder With Rounded Rectangle Placeholder Through",
Text = "This is My Entry",
},
}
{
Opacity = .25d,
BackgroundColor = Colors.Fuchsia,
ActiveColor = Colors.Red,
InactiveColor = Colors.Green,
PlaceholderColor = Colors.Purple,
BorderStyle = ContainerBorderStyle.RoundedRectanglePlaceholderThrough,
Content =
new Entry { Placeholder = "My Placeholder With Rounded Rectangle Placeholder Through", Text = "This is My Entry", },
}
.Assign(out _opacitySil),
new Slider
{
Value = .5d,
Minimum = 0d,
Maximum = 1d,
}
new Slider { Value = .5d, Minimum = 0d, Maximum = 1d, }
.Bind(nameof(IView.Opacity), source: _opacitySil),
new StyledInputLayout
{
@@ -153,10 +126,7 @@ public MainPage(ILogger<TestRxViewModel> logger)
InactiveColor = Colors.Green,
BorderStyle = ContainerBorderStyle.RoundedUnderline,
Content =
new NumericEntry
{
Placeholder = "This must be a numeric value...",
},
new NumericEntry { Placeholder = "This must be a numeric value...", },
},
new StyledInputLayout
{
@@ -165,10 +135,7 @@ public MainPage(ILogger<TestRxViewModel> logger)
InactiveColor = Colors.Green,
BorderStyle = ContainerBorderStyle.RoundedUnderline,
Content =
new NumericEntry
{
Placeholder = "This must be a numeric value...",
}
new NumericEntry { Placeholder = "This must be a numeric value...", }
.Assign(out _rxNumericEntry),
},
new StyledInputLayout
@@ -179,11 +146,7 @@ public MainPage(ILogger<TestRxViewModel> logger)
BorderStyle = ContainerBorderStyle.RoundedUnderline,
InternalMargin = new Thickness(16, 8),
Content =
new NumericEntry
{
Placeholder = "This must be an int value...",
ValueType = NumericEntryValueType.Int,
}
new NumericEntry { Placeholder = "This must be an int value...", ValueType = NumericEntryValueType.Int, }
.Assign(out _rxNumericIntEntry),
},
new StyledInputLayout
@@ -195,13 +158,7 @@ public MainPage(ILogger<TestRxViewModel> logger)
new Picker
{
ItemsSource =
new[]
{
"Item 1",
"Item 2",
"Item 3",
"Item 4",
},
new[] { "Item 1", "Item 2", "Item 3", "Item 4", },
},
},
new StyledInputLayout
@@ -210,41 +167,24 @@ public MainPage(ILogger<TestRxViewModel> logger)
BackgroundColor = Colors.Chartreuse,
BorderStyle = ContainerBorderStyle.RoundedRectangle,
Content =
new DatePicker
{
},
new DatePicker { },
},
new StyledInputLayout
{
Placeholder = "My Editor",
BackgroundColor = Colors.Chartreuse,
BorderStyle = ContainerBorderStyle.Rectangle,
Content =
new Editor
{
Placeholder = "Test Entry",
AutoSize = EditorAutoSizeOption.TextChanges,
},
new Editor { Placeholder = "Test Entry", AutoSize = EditorAutoSizeOption.TextChanges, },
},
new LinearGauge
{
StartingPercent = 10.1d,
EndingPercent = 40.4d,
ProgressBackgroundColor = Colors.Fuchsia,
ProgressColor = Colors.Chartreuse,
},
new CircularFillGauge
{
ProgressPercentage = 46.1d,
ProgressBackgroundColor = Colors.Fuchsia,
ProgressColor = Colors.Chartreuse,
StartingPercent = 10.1d, EndingPercent = 40.4d, ProgressBackgroundColor = Colors.Fuchsia, ProgressColor = Colors.Chartreuse,
},
new CircularFillGauge { ProgressPercentage = 46.1d, ProgressBackgroundColor = Colors.Fuchsia, ProgressColor = Colors.Chartreuse, },
new CircularGauge
{
StartingDegree = 10.1d,
EndingDegree = 90.0d,
ProgressBackgroundColor = Colors.Fuchsia,
ProgressColor = Colors.Chartreuse,
StartingDegree = 10.1d, EndingDegree = 90.0d, ProgressBackgroundColor = Colors.Fuchsia, ProgressColor = Colors.Chartreuse,
},
(_rainbowRing = new Loading.RainbowRing
{
@@ -371,24 +311,16 @@ public MainPage(ILogger<TestRxViewModel> logger)
},
},
}),
new Tile
{
EmbeddedImageName = "triforce.svg",
ButtonBackgroundColor = Colors.Fuchsia,
},
new SvgImageView
{
EmbeddedImageName = "splatoon.svg",
OverlayColor = Colors.Chartreuse,
},
new Tile { EmbeddedImageName = "triforce.svg", ButtonBackgroundColor = Colors.Fuchsia, },
new SvgImageView { EmbeddedImageName = "splatoon.svg", OverlayColor = Colors.Chartreuse, },
new GradientPillButton
{
Text = "Gradient Pill Button",
ButtonBackgroundStartColor = Colors.Fuchsia,
ButtonBackgroundEndColor = Colors.Chartreuse,
FontColor = Colors.DarkRed,
FontFamily = "Clathing",
}
{
Text = "Gradient Pill Button",
ButtonBackgroundStartColor = Colors.Fuchsia,
ButtonBackgroundEndColor = Colors.Chartreuse,
FontColor = Colors.DarkRed,
FontFamily = "Clathing",
}
.Assign(out _pillButton),
new Image()
.SetSvgIcon("splatoon.svg", 66, Colors.Red),
@@ -397,7 +329,15 @@ public MainPage(ILogger<TestRxViewModel> logger)
},
};

this.Bind(ViewModel, vm => vm.NullableDoubleValue, ui => ui._rxNumericEntry.Text);
this._viewImageProcessingButton.Clicked +=
async (sender, args) => await this.Navigation.PushAsync(new ImageProcessing());

this.Bind(
ViewModel,
vm => vm.NullableDoubleValue,
ui => ui._rxNumericEntry.Text,
x => x?.ToString("N2") ?? string.Empty,
x => double.TryParse(x, out var parsed) ? parsed : null);
this.Bind(ViewModel, vm => vm.NullableIntValue, ui => ui._rxNumericIntEntry.Text);

Observable
19 changes: 15 additions & 4 deletions AuroraControlsMaui/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
using AuroraControls;

[assembly: Microsoft.Maui.Controls.XmlnsPrefix("http://auroracontrols.maui/schemas/controls", "AuroraControls")]
[assembly: Microsoft.Maui.Controls.XmlnsPrefix("http://auroracontrols.maui/schemas/controls", "AuroraControls.Gauges")]
[assembly: Microsoft.Maui.Controls.XmlnsPrefix("http://auroracontrols.maui/schemas/controls", "AuroraControls.Loading")]
[assembly: Microsoft.Maui.Controls.XmlnsDefinition("http://auroracontrols.maui/schemas/controls", "AuroraControls")]
[assembly: Microsoft.Maui.Controls.XmlnsPrefix(Constants.XamlNamespace, Constants.CommunityToolkitNamespacePrefix + nameof(AuroraControls.Gauges))]
[assembly: Microsoft.Maui.Controls.XmlnsPrefix(Constants.XamlNamespace, Constants.CommunityToolkitNamespacePrefix + nameof(AuroraControls.Loading))]
[assembly: Microsoft.Maui.Controls.XmlnsPrefix(Constants.XamlNamespace, Constants.CommunityToolkitNamespacePrefix + nameof(AuroraControls.VisualEffects))]
[assembly: Microsoft.Maui.Controls.XmlnsPrefix(Constants.XamlNamespace, Constants.CommunityToolkitNamespace)]

[assembly: Microsoft.Maui.Controls.XmlnsDefinition(Constants.XamlNamespace, "aurora")]

namespace AuroraControls;

#pragma warning disable SA1649
internal static class Constants
#pragma warning restore SA1649
{
public const string XamlNamespace = "http://auroracontrols.maui/schemas/controls";
public const string CommunityToolkitNamespace = $"{nameof(AuroraControls)}";
public const string CommunityToolkitNamespacePrefix = $"{CommunityToolkitNamespace}.";
}
9 changes: 9 additions & 0 deletions AuroraControlsMaui/AuroraControlBuilder.cs
Original file line number Diff line number Diff line change
@@ -21,6 +21,15 @@ public static MauiAppBuilder UseAuroraControls(this MauiAppBuilder mauiAppBuilde
{
mauiHandlersCollection.AddHandler(typeof(StyledInputLayout), typeof(StyledInputLayoutHandler));
mauiHandlersCollection.AddHandler(typeof(NumericEntry), typeof(NumericEntryHandler));
})
.ConfigureEffects(
effects =>
{
#if ANDROID
effects.Add<Effects.ImageProcessingEffect, Effects.ImagePlatformProcessingEffect>();
#elif IOS || MACCATALYST
effects.Add<Effects.ImageProcessingEffect, Effects.ImagePlatformProcessingEffect>();
#endif
});

foreach (var assembly in resourceAssemblies)
345 changes: 345 additions & 0 deletions AuroraControlsMaui/Effects/ImageProcessingEffect.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,345 @@
using Microsoft.Maui.Controls.Platform;

#if ANDROID
using Android.Graphics.Drawables;
using Android.Widget;
using SkiaSharp.Views.Android;
#endif

#if IOS || MACCATALYST
using SkiaSharp.Views.iOS;
using UIKit;
#endif

namespace AuroraControls.Effects;

/// <summary>
/// Image processing effect.
/// </summary>
public class ImageProcessingEffect : RoutingEffect
{
/// <summary>
/// Gets the image processing effects.
/// </summary>
/// <value>The image processing effects.</value>
public ImageProcessing.ImageProcessingCollection ImageProcessingEffects { get; private set; }
= new ImageProcessing.ImageProcessingCollection();

/// <summary>
/// The processor changed property.
/// </summary>
public static readonly BindablePropertyKey ProcessorChangedProperty =
BindableProperty.CreateReadOnly("ProcessorChanged", typeof(object), typeof(ImageProcessingEffect), null);

/// <summary>
/// Initializes a new instance of the <see cref="ImageProcessingEffect"/> class.
/// </summary>
public ImageProcessingEffect()
{
}
}

#if ANDROID
public class ImagePlatformProcessingEffect : PlatformEffect
{
private SKBitmap _image;

private bool _processing;

protected override async void OnAttached()
{
var view = Control as ImageView;

if (view == null)
{
return;
}

if (this.Element?.Effects?.FirstOrDefault(e => e is ImageProcessingEffect) is ImageProcessingEffect effect)
{
effect.ImageProcessingEffects.PropertyChanged += ImageProcessingEffects_PropertyChanged;
}

var drawable = view?.Drawable as BitmapDrawable;

var androidBitmap = drawable?.Bitmap;

_image = androidBitmap?.ToSKBitmap();

await ProcessImage(view, Element);
}

protected override void OnDetached()
{
if (this.Element?.Effects?.FirstOrDefault(e => e is ImageProcessingEffect) is ImageProcessingEffect effect)
{
effect.ImageProcessingEffects.PropertyChanged -= ImageProcessingEffects_PropertyChanged;
}
}

private async void ImageProcessingEffects_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs args)
{
var view = Control as ImageView;

if (view == null)
{
return;
}

if (args.PropertyName.Equals(ImageProcessingEffect.ProcessorChangedProperty.BindableProperty.PropertyName))
{
await ProcessImage(view, Element);
}
}

protected override async void OnElementPropertyChanged(System.ComponentModel.PropertyChangedEventArgs args)
{
var view = Control as ImageView;

if (view == null)
{
return;
}

if (args.PropertyName.Equals(Image.SourceProperty.PropertyName) ||
args.PropertyName.Equals(Image.IsLoadingProperty.PropertyName))
{
var drawable = view?.Drawable as BitmapDrawable;

var androidBitmap = drawable?.Bitmap;

_image = androidBitmap?.ToSKBitmap();

await ProcessImage(view, Element);
}

base.OnElementPropertyChanged(args);
}

private async Task ProcessImage(ImageView view, Element element)
{
if (_processing)
{
return;
}

_processing = true;

try
{
if (element == null)
{
return;
}

if (_image == null)
{
view.SetImageBitmap(null);
return;
}

var effect = Element?.Effects?.FirstOrDefault(e => e is ImageProcessingEffect) as ImageProcessingEffect;

var processingEffects = effect?.ImageProcessingEffects;

if (!processingEffects?.Any() ?? true)
{
return;
}

SKBitmap processingImage = null;

try
{
processingImage = _image.Copy();

await Task.Run(() =>
{
foreach (var processingEffect in processingEffects)
{
var imageProcessor = ImageProcessing.RegisteredImageProcessors.GetProcessor(processingEffect.Key);

if (imageProcessor != null)
{
var tempImage = imageProcessor.ProcessImage(processingImage, processingEffect);

if (tempImage != processingImage)
{
processingImage?.Dispose();
processingImage = null;
}

processingImage = tempImage;
}
}
});

using var native = processingImage.ToBitmap();
(view?.Drawable as BitmapDrawable)?.Bitmap?.Recycle();
view.SetImageBitmap(native);
}
finally
{
processingImage?.Dispose();
processingImage = null;
}
}
finally
{
_processing = false;
}
}
}
#endif

#if IOS || MACCATALYST
public class ImagePlatformProcessingEffect : PlatformEffect
{
private SKBitmap _image;

private bool _processing;

private long _lastProcessingTime;

protected override async void OnAttached()
{
var view = Control as UIImageView;

if (view == null)
{
return;
}

if (this.Element?.Effects?.FirstOrDefault(e => e is Effects.ImageProcessingEffect) is ImageProcessingEffect effect)
{
effect.ImageProcessingEffects.PropertyChanged += ImageProcessingEffects_PropertyChanged;
}

_image = view?.Image?.ToSKBitmap();
await ProcessImage(view, Element);
}

protected override void OnDetached()
{
if (this.Element?.Effects?.FirstOrDefault(e => e is ImageProcessingEffect) is ImageProcessingEffect effect)
{
effect.ImageProcessingEffects.PropertyChanged -= ImageProcessingEffects_PropertyChanged;
}
}

private async void ImageProcessingEffects_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs args)
{
var view = Control as UIImageView;

if (view == null)
{
return;
}

if (args.PropertyName.Equals(ImageProcessingEffect.ProcessorChangedProperty.BindableProperty.PropertyName))
{
_lastProcessingTime = DateTime.UtcNow.Ticks;
await ProcessImage(view, Element);
}
}

protected override async void OnElementPropertyChanged(System.ComponentModel.PropertyChangedEventArgs args)
{
var view = Control as UIImageView;

if (view == null)
{
return;
}

if (args.PropertyName.Equals(Image.SourceProperty.PropertyName) ||
args.PropertyName.Equals(Image.IsLoadingProperty.PropertyName))
{
_image = view?.Image?.ToSKBitmap();

await ProcessImage(view, Element);
}

base.OnElementPropertyChanged(args);
}

private async Task ProcessImage(UIImageView view, Element element)
{
if (_processing)
{
return;
}

_processing = true;
var currentProcessingTime = _lastProcessingTime;

try
{
if (element == null)
{
return;
}

if (_image == null)
{
view.Image = null;
return;
}

var effect = Element?.Effects?.FirstOrDefault(e => e is ImageProcessingEffect) as ImageProcessingEffect;

var processingEffects = effect?.ImageProcessingEffects;

if (!processingEffects?.Any() ?? true)
{
return;
}

SKBitmap processingImage = null;
try
{
processingImage = _image.Copy();

await Task.Run(() =>
{
foreach (var processingEffect in processingEffects)
{
var imageProcessor = ImageProcessing.RegisteredImageProcessors.GetProcessor(processingEffect.Key);

if (imageProcessor != null)
{
var tempImage = imageProcessor.ProcessImage(processingImage, processingEffect);

if (tempImage != processingImage)
{
processingImage?.Dispose();
processingImage = null;
}

processingImage = tempImage;
}
}
});

view.Image?.Dispose();

using var native = processingImage?.ToUIImage();
view.Image = native;
}
finally
{
processingImage?.Dispose();
processingImage = null;
}
}
finally
{
_processing = false;
if (_lastProcessingTime > currentProcessingTime)
{
await ProcessImage(view, element);
}
}
}
}
#endif
103 changes: 103 additions & 0 deletions AuroraControlsMaui/ImageProcessing/Blur.full.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
using System;
using SkiaSharp;

namespace AuroraControls.ImageProcessing;

/// <summary>
/// Blur effect for images.
/// </summary>
public class Blur : ImageProcessingBase, IImageProcessor
{
/// <summary>
/// Gets the key.
/// </summary>
/// <value>The "Blur" key.</value>
public override string Key => nameof(Blur);

/// <summary>
/// Blur location options.
/// </summary>
public enum BlurLocation
{
Full,
Inside,
}

/// <summary>
/// The blur amount property.
/// </summary>
public static BindableProperty BlurAmountProperty =
BindableProperty.Create(nameof(BlurAmount), typeof(double), typeof(Blur), default(double));

/// <summary>
/// Gets or sets the blur amount.
/// </summary>
/// <value>A double value representing the blur amount. Default value is default(double).</value>
public double BlurAmount
{
get { return (double)GetValue(BlurAmountProperty); }
set { SetValue(BlurAmountProperty, value); }
}

/// <summary>
/// The blurring location property.
/// </summary>
public static BindableProperty BlurringLocationProperty =
BindableProperty.Create(nameof(BlurringLocation), typeof(object), typeof(BlurLocation), BlurLocation.Full);

/// <summary>
/// Gets or sets the blurring location.
/// </summary>
/// <value>Takes a BlurLocation enum. Default value is BlurLocation.Full.</value>
public BlurLocation BlurringLocation
{
get { return (BlurLocation)GetValue(BlurringLocationProperty); }
set { SetValue(BlurringLocationProperty, value); }
}

/// <summary>
/// Processes the image and apply blur.
/// </summary>
/// <returns>The an SKBitmap image.</returns>
/// <param name="processingImage">Processing SKBitmap image.</param>
/// <param name="imageProcessor">Image processor.</param>
public SKBitmap ProcessImage(SKBitmap processingImage, ImageProcessingBase imageProcessor)
{
if (imageProcessor is not AuroraControls.ImageProcessing.Blur)
{
return processingImage;
}

var blur = imageProcessor as AuroraControls.ImageProcessing.Blur;

var blurAmount = (float)blur.BlurAmount;
var blurLocation = blur.BlurringLocation;

var bitmap = new SKBitmap(processingImage.Info);

using var canvas = new SKCanvas(bitmap);
using var paint = new SKPaint();
paint.IsAntialias = true;
paint.Style = SKPaintStyle.Fill;

if (blurLocation == AuroraControls.ImageProcessing.Blur.BlurLocation.Inside)
{
paint.BlendMode = SKBlendMode.SrcOver;
}

paint.ImageFilter = SKImageFilter.CreateBlur((int)blurAmount, (int)blurAmount);

canvas.Clear();

if (blurLocation == AuroraControls.ImageProcessing.Blur.BlurLocation.Full)
{
canvas.DrawBitmap(processingImage, processingImage.Info.Rect);
}

canvas.DrawBitmap(processingImage, processingImage.Info.Rect, paint);

canvas.Flush();

return bitmap;
}
}
59 changes: 59 additions & 0 deletions AuroraControlsMaui/ImageProcessing/Circular.full.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using System;
using SkiaSharp;

namespace AuroraControls.ImageProcessing;

/// <summary>
/// Circular image mask.
/// </summary>
public class Circular : ImageProcessingBase, IImageProcessor
{
/// <summary>
/// Gets the key.
/// </summary>
/// <value>The "Circular" key.</value>
public override string Key => nameof(Circular);

/// <summary>
/// Processes the image and apply circular mask.
/// </summary>
/// <returns>The an SKBitmap image.</returns>
/// <param name="processingImage">Processing SKBitmap image.</param>
/// <param name="imageProcessor">Image processor.</param>
public SKBitmap ProcessImage(SKBitmap processingImage, ImageProcessingBase imageProcessor)
{
if (imageProcessor is not AuroraControls.ImageProcessing.Circular)
{
return processingImage;
}

using var canvas = new SKCanvas(processingImage);
using var paint = new SKPaint();
paint.BlendMode = SKBlendMode.SrcIn;
paint.IsAntialias = true;
paint.Color = SKColors.Transparent;

var size = Math.Min(processingImage.Info.Width, processingImage.Info.Height);

var left = (processingImage.Info.Width - size) / 2f;
var top = (processingImage.Info.Height - size) / 2f;
var right = left + size;
var bottom = top + size;

var rect = new SKRect(left, top, right, bottom);

using (var outer = new SKPath())
using (var cutout = new SKPath())
{
outer.AddRect(processingImage.Info.Rect);
cutout.AddOval(rect);
using (var finalPath = outer.Op(cutout, SKPathOp.Difference))
{
canvas.DrawPath(finalPath, paint);
canvas.Flush();
}
}

return processingImage;
}
}
41 changes: 41 additions & 0 deletions AuroraControlsMaui/ImageProcessing/Grayscale.full.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
namespace AuroraControls.ImageProcessing;

/// <summary>
/// Grayscale image effect.
/// </summary>
public class Grayscale : ImageProcessingBase, IImageProcessor
{
/// <summary>
/// Gets the key.
/// </summary>
/// <value>The "Grayscale" key.</value>
public override string Key => nameof(Grayscale);

/// <summary>
/// Apply grayscale filter.
/// </summary>
/// <returns>The an SKBitmap image.</returns>
/// <param name="processingImage">Processing SKBitmap image.</param>
/// <param name="imageProcessor">Image processor.</param>
public SKBitmap ProcessImage(SKBitmap processingImage, ImageProcessingBase imageProcessor)
{
var bitmap = new SKBitmap(processingImage.Info);

using var canvas = new SKCanvas(bitmap);
using var paint = new SKPaint();

paint.ColorFilter = SKColorFilter.CreateColorMatrix(
[
0.21f, 0.72f, 0.07f, 0.0f, 0.0f,
0.21f, 0.72f, 0.07f, 0.0f, 0.0f,
0.21f, 0.72f, 0.07f, 0.0f, 0.0f,
0.0f, 0.0f, 0.0f, 1.0f, 0.0f
]);

canvas.Clear();
canvas.DrawBitmap(processingImage, processingImage.Info.Rect, paint);
canvas.Flush();

return bitmap;
}
}
18 changes: 18 additions & 0 deletions AuroraControlsMaui/ImageProcessing/IImageProcessor.core.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System;
using SkiaSharp;

namespace AuroraControls.ImageProcessing;

/// <summary>
/// Image processor interface.
/// </summary>
public interface IImageProcessor
{
/// <summary>
/// Processes the image.
/// </summary>
/// <returns>The image.</returns>
/// <param name="processingImage">Processing image.</param>
/// <param name="imageProcessor">Image processor.</param>
SKBitmap ProcessImage(SKBitmap processingImage, ImageProcessingBase imageProcessor);
}
13 changes: 13 additions & 0 deletions AuroraControlsMaui/ImageProcessing/ImageProcessingBase.core.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace AuroraControls.ImageProcessing;

/// <summary>
/// Image processing base class.
/// </summary>
public abstract class ImageProcessingBase : BindableObject
{
/// <summary>
/// Gets the key.
/// </summary>
/// <value>The key for processor.</value>
public abstract string Key { get; }
}
238 changes: 238 additions & 0 deletions AuroraControlsMaui/ImageProcessing/ImageProcessingCollection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;

namespace AuroraControls.ImageProcessing;

/// <summary>
/// Image processing collection.
/// </summary>
public class ImageProcessingCollection : BindableObject, IList<ImageProcessingBase>, INotifyCollectionChanged
{
/// <summary>
/// The collection of image processors.
/// </summary>
private readonly List<ImageProcessingBase> _items = new List<ImageProcessingBase>();

/// <summary>
/// Occurs when collection changed.
/// </summary>
public event NotifyCollectionChangedEventHandler CollectionChanged;

/// <summary>
/// Finalizes an instance of the <see cref="ImageProcessingCollection"/> class.
/// Releases unmanaged resources and performs other cleanup operations before the
/// <see cref="T:AuroraControls.ImageProcessing.ImageProcessingCollection"/> is reclaimed by garbage collection.
/// </summary>
~ImageProcessingCollection()
{
if (this._items == null)
{
return;
}

foreach (var item in this._items)
{
item.PropertyChanged -= this.HandlePropertyChangedEventHandler;
}
}

/// <summary>
/// Gets the index in the collection of a given processor.
/// </summary>
/// <returns>The index of the processor.</returns>
/// <param name="item">Item.</param>
public int IndexOf(ImageProcessingBase item)
{
return this._items.IndexOf(item);
}

/// <summary>
/// Inserts the processor at the specified index.
/// </summary>
/// <param name="index">Index to insert at.</param>
/// <param name="item">Image processor.</param>
public void Insert(int index, ImageProcessingBase item)
{
this._items.Insert(index, item);

item.PropertyChanged -= HandlePropertyChangedEventHandler;
item.PropertyChanged += HandlePropertyChangedEventHandler;

SetValue(Effects.ImageProcessingEffect.ProcessorChangedProperty, item);
CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index));
}

/// <summary>
/// Removes processor at index.
/// </summary>
/// <param name="index">Index.</param>
public void RemoveAt(int index)
{
var oldItem = this[index];
this._items.RemoveAt(index);
oldItem.PropertyChanged -= HandlePropertyChangedEventHandler;
SetValue(Effects.ImageProcessingEffect.ProcessorChangedProperty, oldItem);
CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, oldItem, index));
}

/// <summary>
/// Gets or sets the <see cref="T:AuroraControls.ImageProcessing.ImageProcessingCollection"/> at the specified index.
/// </summary>
/// <param name="index">Index.</param>
public ImageProcessingBase this[int index]
{
get
{
return this._items[index];
}

set
{
var oldItem = this[index];

var imageProcessingBase = (ImageProcessingBase)value;

this._items[index] = imageProcessingBase;

imageProcessingBase.PropertyChanged -= HandlePropertyChangedEventHandler;
imageProcessingBase.PropertyChanged += HandlePropertyChangedEventHandler;

oldItem.PropertyChanged -= HandlePropertyChangedEventHandler;
SetValue(Effects.ImageProcessingEffect.ProcessorChangedProperty, oldItem);
CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, value, oldItem));
}
}

/// <summary>
/// Add the specified processor to the list.
/// </summary>
/// <param name="item">Image processor.</param>
public void Add(ImageProcessingBase item)
{
this._items.Add(item);

item.PropertyChanged -= HandlePropertyChangedEventHandler;
item.PropertyChanged += HandlePropertyChangedEventHandler;

SetValue(Effects.ImageProcessingEffect.ProcessorChangedProperty, item);
CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, Count - 1));
}

/// <summary>
/// Clears the list.
/// </summary>
public void Clear()
{
foreach (var item in this._items)
{
item.PropertyChanged -= HandlePropertyChangedEventHandler;
}

this._items.Clear();

SetValue(Effects.ImageProcessingEffect.ProcessorChangedProperty, null);
CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}

/// <summary>
/// Checks if the processor passed in already exists in the list.
/// </summary>
/// <returns>Returns <c>true</c> if the item exists in the list, otherwise <c>false</c>.</returns>
/// <param name="item">Item.</param>
public bool Contains(ImageProcessingBase item)
{
return this._items.Contains(item);
}

/// <summary>
/// Copies items to list.
/// </summary>
/// <param name="array">Array of processors.</param>
/// <param name="arrayIndex">Array index.</param>
public void CopyTo(ImageProcessingBase[] array, int arrayIndex)
{
foreach (var item in array)
{
item.PropertyChanged -= HandlePropertyChangedEventHandler;
item.PropertyChanged += HandlePropertyChangedEventHandler;
}

this._items.CopyTo(array, arrayIndex);
}

/// <summary>
/// Remove the specified item.
/// </summary>
/// <returns>The remove.</returns>
/// <param name="item">Item.</param>
public bool Remove(ImageProcessingBase item)
{
var oldIndex = IndexOf(item);

if (!this._items.Remove(item))
{
return false;
}

item.PropertyChanged -= this.HandlePropertyChangedEventHandler;
this.SetValue(Effects.ImageProcessingEffect.ProcessorChangedProperty, item);
this.CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, oldIndex));
return true;
}

/// <summary>
/// Gets the count.
/// </summary>
/// <value>The count.</value>
public int Count
{
get
{
return this._items.Count;
}
}

/// <summary>
/// Gets a value indicating whether this <see cref="T:AuroraControls.ImageProcessing.ImageProcessingCollection"/> is
/// read only.
/// </summary>
/// <value><c>true</c> if is read only; otherwise, <c>false</c>.</value>
public bool IsReadOnly
{
get
{
return false;
}
}

/// <summary>
/// Gets the enumerator.
/// </summary>
/// <returns>The enumerator.</returns>
public IEnumerator<ImageProcessingBase> GetEnumerator()
{
return this._items.GetEnumerator();
}

/// <summary>
/// System.s the collections. IE numerable. get enumerator.
/// </summary>
/// <returns>The collections. IE numerable. get enumerator.</returns>
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}

/// <summary>
/// Handles the property changed event handler.
/// </summary>
/// <param name="sender">Sender.</param>
/// <param name="e"><c>PropertyChangedEventArgs</c> provides the <see cref="T:PropertyChangedEventArgs.PropertyName"/> property to get the name of the property that changed.</param>
private void HandlePropertyChangedEventHandler(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
ClearValue(Effects.ImageProcessingEffect.ProcessorChangedProperty);
SetValue(Effects.ImageProcessingEffect.ProcessorChangedProperty, sender);
}
}
45 changes: 45 additions & 0 deletions AuroraControlsMaui/ImageProcessing/Invert.full.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using System;
using SkiaSharp;

namespace AuroraControls.ImageProcessing;

/// <summary>
/// Invert image processing effect.
/// </summary>
public class Invert : ImageProcessingBase, IImageProcessor
{
/// <summary>
/// Gets the key.
/// </summary>
/// <value>The "Invert" key.</value>
public override string Key => nameof(Invert);

/// <summary>
/// Processes the inversion image.
/// </summary>
/// <returns>An SKBitmap image.</returns>
/// <param name="processingImage">Image to process.</param>
/// <param name="imageProcessor">Image processor.</param>
public SKBitmap ProcessImage(SKBitmap processingImage, ImageProcessingBase imageProcessor)
{
using (var canvas = new SKCanvas(processingImage))
using (var paint = new SKPaint())
{
paint.IsAntialias = true;
paint.Style = SKPaintStyle.Fill;
paint.ColorFilter = SKColorFilter.CreateColorMatrix(
[
-1f, 0f, 0f, 0f, 255f,
0f, -1f, 0f, 0f, 255f,
0f, 0f, -1f, 0f, 255f,
0f, 0f, 0f, 1f, 0f
]);

canvas.Clear();
canvas.DrawBitmap(processingImage, processingImage.Info.Rect, paint);
canvas.Flush();
}

return processingImage;
}
}
47 changes: 47 additions & 0 deletions AuroraControlsMaui/ImageProcessing/RegisteredImageProcessors.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;

namespace AuroraControls.ImageProcessing;

/// <summary>
/// Registered image processors.
/// </summary>
public static class RegisteredImageProcessors
{
/// <summary>
/// A static dictionary of registered Image processors and their adjacent keys.
/// </summary>
private static readonly Dictionary<string, IImageProcessor> _imageProcessors = new Dictionary<string, IImageProcessor>();

static RegisteredImageProcessors()
{
SetProcessor(nameof(Grayscale), new Grayscale());
SetProcessor(nameof(Sepia), new Sepia());
SetProcessor(nameof(Invert), new Invert());
SetProcessor(nameof(Grayscale), new Grayscale());
SetProcessor(nameof(Blur), new Blur());
SetProcessor(nameof(Circular), new Circular());
SetProcessor(nameof(Scale), new Scale());
SetProcessor(nameof(ResizeImage), new ResizeImage());
}

/// <summary>
/// Gets the processor by key.
/// </summary>
/// <returns>result as an IImageProcessor.</returns>
/// <param name="key">Key/name of processor.</param>
public static IImageProcessor GetProcessor(string key)
{
return _imageProcessors.ContainsKey(key) ? _imageProcessors[key] : null;
}

/// <summary>
/// Sets the processor.
/// </summary>
/// <param name="key">Key.</param>
/// <param name="imageProcessor">Image processor.</param>
public static void SetProcessor(string key, IImageProcessor imageProcessor)
{
_imageProcessors[key] = imageProcessor;
}
}
164 changes: 164 additions & 0 deletions AuroraControlsMaui/ImageProcessing/ResizeImage.full.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
using System;
using System.IO;
using SkiaSharp;

namespace AuroraControls.ImageProcessing;

/// <summary>
/// Resize image processor.
/// </summary>
public class ResizeImage : ImageProcessingBase, IImageProcessor
{
/// <summary>
/// Gets the key.
/// </summary>
/// <value>The "ResizeImage" key.</value>
public override string Key => nameof(ResizeImage);

/// <summary>
/// The max height property.
/// </summary>
public static BindableProperty MaxHeightProperty =
BindableProperty.Create(nameof(MaxHeight), typeof(int), typeof(ResizeImage), 100);

/// <summary>
/// Gets or sets the height of the max.
/// </summary>
/// <value>int value representing the desired max height.</value>
public int MaxHeight
{
get { return (int)GetValue(MaxHeightProperty); }
set { SetValue(MaxHeightProperty, value); }
}

/// <summary>
/// The max width property.
/// </summary>
public static BindableProperty MaxWidthProperty =
BindableProperty.Create(nameof(MaxWidth), typeof(int), typeof(ResizeImage), 100);

/// <summary>
/// Gets or sets the width of the max.
/// </summary>
/// <value>int value processing the desired max width.</value>
public int MaxWidth
{
get { return (int)GetValue(MaxWidthProperty); }
set { SetValue(MaxWidthProperty, value); }
}

/// <summary>
/// Processes the image.
/// </summary>
/// <returns>The SKBitmap image.</returns>
/// <param name="processingImage">Processing image.</param>
/// <param name="imageProcessor">Image processor.</param>
public SKBitmap ProcessImage(SKBitmap processingImage, ImageProcessingBase imageProcessor)
{
if (imageProcessor is AuroraControls.ImageProcessing.ResizeImage)
{
var resizeImageProcessor = imageProcessor as AuroraControls.ImageProcessing.ResizeImage;
var maxHeight = resizeImageProcessor.MaxHeight;
var maxWidth = resizeImageProcessor.MaxWidth;

var info = processingImage.Info;

var maxSize = Math.Max(maxHeight, maxWidth);

var supportedScale =
info.Height > info.Width
? (float)maxHeight / info.Height
: (float)maxWidth / info.Width;

var scaledWidth = (int)(info.Width * supportedScale);
var scaledHeight = (int)(info.Height * supportedScale);

var newImageInfo = new SKImageInfo(scaledWidth, scaledHeight, processingImage.Info.ColorType);

return processingImage.Resize(newImageInfo, SKFilterQuality.High);
}

return processingImage;
}

/// <summary>
/// Resizes the image for export as stream.
/// </summary>
/// <returns>The image exported as a stream.</returns>
/// <param name="imageBytes">Image bytes.</param>
/// <param name="maxHeight">Max height.</param>
/// <param name="maxWidth">Max width.</param>
/// <param name="quality">Quality.</param>
/// <param name="imageFormat">Image format; default is PNG.</param>
/// <param name="streamDisposesData">If set to <c>true</c> stream disposes data.</param>
public static Stream ResizeImageForExportAsStream(byte[] imageBytes, int maxHeight = 100, int maxWidth = 100, int quality = 80, SKEncodedImageFormat imageFormat = SKEncodedImageFormat.Png, bool streamDisposesData = true)
{
return ResizeImageInternal(imageBytes, maxHeight, maxWidth, quality, imageFormat).AsStream(streamDisposesData);
}

/// <summary>
/// Resizes the image for export as byte[].
/// </summary>
/// <returns>The image exported as byte[].</returns>
/// <param name="imageBytes">Image bytes.</param>
/// <param name="maxHeight">Max height.</param>
/// <param name="maxWidth">Max width.</param>
/// <param name="quality">Quality.</param>
/// <param name="imageFormat">Image format.</param>
public static byte[] ResizeImageForExport(byte[] imageBytes, int maxHeight = 100, int maxWidth = 100, int quality = 80, SKEncodedImageFormat imageFormat = SKEncodedImageFormat.Png)
{
return ResizeImageInternal(imageBytes, maxHeight, maxWidth, quality, imageFormat).ToArray();
}

/// <summary>
/// Internal method used for resizeing the image.
/// </summary>
/// <returns>The image as SKData.</returns>
/// <param name="imageBytes">Image bytes.</param>
/// <param name="maxHeight">Max height.</param>
/// <param name="maxWidth">Max width.</param>
/// <param name="quality">Quality.</param>
/// <param name="imageFormat">Image format.</param>
private static SKData ResizeImageInternal(byte[] imageBytes, int maxHeight = 100, int maxWidth = 100, int quality = 80, SKEncodedImageFormat imageFormat = SKEncodedImageFormat.Png)
{
using (SKData data = SKData.CreateCopy(imageBytes))
{
using (SKCodec codec = SKCodec.Create(data))
{
var info = codec.Info;

var maxSize = Math.Max(maxHeight, maxWidth);

var supportedScale =
info.Height > info.Width
? (float)maxHeight / info.Height
: (float)maxWidth / info.Width;

var scaledWidth = (int)(info.Width * supportedScale);
var scaledHeight = (int)(info.Height * supportedScale);

// decode the bitmap at the nearest size
var nearest = new SKImageInfo(scaledWidth, scaledHeight);

SKBitmap bmp = null;
try
{
bmp = SKBitmap.Decode(codec);

SKImageInfo desired = new SKImageInfo(scaledWidth, scaledHeight);
bmp = bmp.Resize(desired, SKFilterQuality.High);

using (var image = SKImage.FromBitmap(bmp))
{
return image.Encode(imageFormat, quality);
}
}
finally
{
bmp?.Dispose();
bmp = null;
}
}
}
}
}
158 changes: 158 additions & 0 deletions AuroraControlsMaui/ImageProcessing/Rotate.full.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
using System;
using System.IO;
using SkiaSharp;

namespace AuroraControls.ImageProcessing;

public class Rotate : ImageProcessingBase, IImageProcessor
{
public enum RotationDegrees
{
Zero = 0,
Ninety = 90,
OneHundredAndEighty = 180,
TwoHundredAndSeventy = 270,
NegativeNinety = -90,
NegativeOneHundredAndEighty = -180,
NegativeTwoHundredAndSeventy = -270,
}

/// <summary>
/// Gets the key.
/// </summary>
/// <value>The "ResizeImage" key.</value>
public override string Key => nameof(Rotate);

public static BindableProperty RotationAmountProperty =
BindableProperty.Create(nameof(RotationDegrees), typeof(object), typeof(Rotate), RotationDegrees.Zero);

public RotationDegrees RotationAmount
{
get => (RotationDegrees)GetValue(RotationAmountProperty);
set => SetValue(RotationAmountProperty, value);
}

/// <summary>
/// Processes the image.
/// </summary>
/// <returns>The SKBitmap image.</returns>
/// <param name="processingImage">Processing image.</param>
/// <param name="imageProcessor">Image processor.</param>
public SKBitmap ProcessImage(SKBitmap processingImage, ImageProcessingBase imageProcessor)
{
if (imageProcessor is AuroraControls.ImageProcessing.Rotate processor)
{
var width = 0;
var height = 0;

switch (processor.RotationAmount)
{
case RotationDegrees.Ninety:
case RotationDegrees.TwoHundredAndSeventy:
case RotationDegrees.NegativeNinety:
case RotationDegrees.NegativeTwoHundredAndSeventy:
width = processingImage.Height;
height = processingImage.Width;
break;
default:
height = processingImage.Height;
width = processingImage.Width;
break;
}

var bitmap = new SKBitmap(width, height, processingImage.AlphaType == SKAlphaType.Opaque);
using (var canvas = new SKCanvas(bitmap))
{
canvas.Translate(width, 0);
canvas.RotateDegrees((float)processor.RotationAmount);
canvas.DrawBitmap(processingImage, 0, 0);
canvas.Flush();
}

return bitmap;
}

return processingImage;
}

/// <summary>
/// Resizes the image for export as stream.
/// </summary>
/// <returns>The image exported as a stream.</returns>
/// <param name="imageBytes">Image bytes.</param>
/// <param name="maxHeight">Max height.</param>
/// <param name="maxWidth">Max width.</param>
/// <param name="quality">Quality.</param>
/// <param name="imageFormat">Image format; default is PNG.</param>
/// <param name="streamDisposesData">If set to <c>true</c> stream disposes data.</param>
public static Stream ResizeImageForExportAsStream(byte[] imageBytes, int maxHeight = 100, int maxWidth = 100, int quality = 80, SKEncodedImageFormat imageFormat = SKEncodedImageFormat.Png, bool streamDisposesData = true)
{
return ResizeImageInternal(imageBytes, maxHeight, maxWidth, quality, imageFormat).AsStream(streamDisposesData);
}

/// <summary>
/// Resizes the image for export as byte[].
/// </summary>
/// <returns>The image exported as byte[].</returns>
/// <param name="imageBytes">Image bytes.</param>
/// <param name="maxHeight">Max height.</param>
/// <param name="maxWidth">Max width.</param>
/// <param name="quality">Quality.</param>
/// <param name="imageFormat">Image format.</param>
public static byte[] ResizeImageForExport(byte[] imageBytes, int maxHeight = 100, int maxWidth = 100, int quality = 80, SKEncodedImageFormat imageFormat = SKEncodedImageFormat.Png)
{
return ResizeImageInternal(imageBytes, maxHeight, maxWidth, quality, imageFormat).ToArray();
}

/// <summary>
/// Internal method used for resizeing the image.
/// </summary>
/// <returns>The image as SKData.</returns>
/// <param name="imageBytes">Image bytes.</param>
/// <param name="maxHeight">Max height.</param>
/// <param name="maxWidth">Max width.</param>
/// <param name="quality">Quality.</param>
/// <param name="imageFormat">Image format.</param>
private static SKData ResizeImageInternal(byte[] imageBytes, int maxHeight = 100, int maxWidth = 100, int quality = 80, SKEncodedImageFormat imageFormat = SKEncodedImageFormat.Png)
{
using (SKData data = SKData.CreateCopy(imageBytes))
{
using (SKCodec codec = SKCodec.Create(data))
{
var info = codec.Info;

var maxSize = Math.Max(maxHeight, maxWidth);

var supportedScale =
info.Height > info.Width
? (float)maxHeight / info.Height
: (float)maxWidth / info.Width;

var scaledWidth = (int)(info.Width * supportedScale);
var scaledHeight = (int)(info.Height * supportedScale);

// decode the bitmap at the nearest size
var nearest = new SKImageInfo(scaledWidth, scaledHeight);

SKBitmap bmp = null;
try
{
bmp = SKBitmap.Decode(codec);

SKImageInfo desired = new SKImageInfo(scaledWidth, scaledHeight);
bmp = bmp.Resize(desired, SKFilterQuality.High);

using (var image = SKImage.FromBitmap(bmp))
{
return image.Encode(imageFormat, quality);
}
}
finally
{
bmp?.Dispose();
bmp = null;
}
}
}
}
}
58 changes: 58 additions & 0 deletions AuroraControlsMaui/ImageProcessing/Scale.full.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System;
using SkiaSharp;

namespace AuroraControls.ImageProcessing;

/// <summary>
/// Scale image.
/// </summary>
public class Scale : ImageProcessingBase, IImageProcessor
{
/// <summary>
/// Gets the key.
/// </summary>
/// <value>The "Scale" key.</value>
public override string Key => nameof(Scale);

/// <summary>
/// The scale amount property.
/// </summary>
public static BindableProperty ScaleAmountProperty =
BindableProperty.Create(nameof(ScaleAmount), typeof(double), typeof(Scale), 1d);

/// <summary>
/// Gets or sets the scale amount.
/// </summary>
/// <value>Expects a double. Default value is 1d.</value>
public double ScaleAmount
{
get { return (double)GetValue(ScaleAmountProperty); }
set { SetValue(ScaleAmountProperty, value); }
}

/// <summary>
/// Apply Scale process.
/// </summary>
/// <returns>The an SKBitmap image.</returns>
/// <param name="processingImage">Processing SKBitmap image.</param>
/// <param name="imageProcessor">Image processor.</param>
public SKBitmap ProcessImage(SKBitmap processingImage, ImageProcessingBase imageProcessor)
{
if (imageProcessor is AuroraControls.ImageProcessing.Scale)
{
var scaleProcessor = imageProcessor as AuroraControls.ImageProcessing.Scale;

var scaleAmount = (float)scaleProcessor.ScaleAmount;

var info =
new SKImageInfo(
(int)(processingImage.Info.Rect.Width * scaleAmount),
(int)(processingImage.Info.Rect.Height * scaleAmount),
processingImage.Info.ColorType);

return processingImage.Resize(info, SKFilterQuality.High);
}

return processingImage;
}
}
47 changes: 47 additions & 0 deletions AuroraControlsMaui/ImageProcessing/Sepia.full.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System;
using SkiaSharp;

namespace AuroraControls.ImageProcessing;

/// <summary>
/// Sepia effect.
/// </summary>
public class Sepia : ImageProcessingBase, IImageProcessor
{
/// <summary>
/// Gets the key.
/// </summary>
/// <value>The "Sepia" key.</value>
public override string Key => nameof(Sepia);

/// <summary>
/// Apply Scale process.
/// </summary>
/// <returns>The an SKBitmap image.</returns>
/// <param name="processingImage">Processing SKBitmap image.</param>
/// <param name="imageProcessor">Image processor.</param>
public SKBitmap ProcessImage(SKBitmap processingImage, ImageProcessingBase imageProcessor)
{
var bitmap = new SKBitmap(processingImage.Info);

using (var canvas = new SKCanvas(bitmap))
using (var paint = new SKPaint())
{
paint.ColorFilter = SKColorFilter.CreateColorMatrix(
[
0.393f, 0.769f, 0.189f, 0.0f, 0.0f,
0.349f, 0.686f, 0.168f, 0.0f, 0.0f,
0.272f, 0.534f, 0.131f, 0.0f, 0.0f,
0.0f, 0.0f, 0.0f, 1.0f, 0.0f
]);

canvas.Clear();

canvas.DrawBitmap(processingImage, processingImage.Info.Rect, paint);

canvas.Flush();
}

return bitmap;
}
}
177 changes: 177 additions & 0 deletions AuroraControlsMaui/ImageProcessing/Watermark.full.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
namespace AuroraControls.ImageProcessing;

/// <summary>
/// Watermark effect for images.
/// </summary>
public class Watermark : ImageProcessingBase, IImageProcessor
{
public enum WatermarkLocation
{
Start,
Center,
End,
}

public static BindableProperty BackgroundColorProperty =
BindableProperty.Create(nameof(BackgroundColor), typeof(Color), typeof(Watermark));

public static BindableProperty BackgroundCornerRadiusProperty =
BindableProperty.Create(nameof(BackgroundCornerRadius), typeof(double), typeof(Watermark), 4d);

public static BindableProperty FontSizeProperty =
BindableProperty.Create(nameof(FontSize), typeof(double), typeof(Watermark), 24d);

public static BindableProperty ForegroundColorProperty =
BindableProperty.Create(nameof(ForegroundColor), typeof(Color), typeof(Watermark), Colors.White.MultiplyAlpha(.5f));

public static BindableProperty HorizontalWatermarkLocationProperty =
BindableProperty.Create(nameof(HorizontalWatermarkLocation), typeof(WatermarkLocation), typeof(Watermark), WatermarkLocation.End);

public static BindableProperty TypefaceProperty =
BindableProperty.Create(nameof(Typeface), typeof(SKTypeface), typeof(Watermark));

public static BindableProperty VerticalWatermarkLocationProperty =
BindableProperty.Create(nameof(VerticalWatermarkLocation), typeof(WatermarkLocation), typeof(Watermark), WatermarkLocation.End);

public static BindableProperty WatermarkPaddingProperty =
BindableProperty.Create(nameof(WatermarkPadding), typeof(double), typeof(Watermark), 8d);

public static BindableProperty WatermarkTextProperty =
BindableProperty.Create(nameof(WatermarkText), typeof(string), typeof(Watermark));

/// <summary>
/// Gets the key.
/// </summary>
/// <value>The "Watermark" key.</value>
public override string Key => nameof(Watermark);

public WatermarkLocation HorizontalWatermarkLocation
{
get => (WatermarkLocation)this.GetValue(HorizontalWatermarkLocationProperty);
set => this.SetValue(HorizontalWatermarkLocationProperty, value);
}

public WatermarkLocation VerticalWatermarkLocation
{
get => (WatermarkLocation)this.GetValue(VerticalWatermarkLocationProperty);
set => this.SetValue(VerticalWatermarkLocationProperty, value);
}

public double WatermarkPadding
{
get => (double)this.GetValue(WatermarkPaddingProperty);
set => this.SetValue(WatermarkPaddingProperty, value);
}

public string WatermarkText
{
get => (string)this.GetValue(WatermarkTextProperty);
set => this.SetValue(WatermarkTextProperty, value);
}

public double FontSize
{
get => (double)this.GetValue(FontSizeProperty);
set => this.SetValue(FontSizeProperty, value);
}

public SKTypeface Typeface
{
get => (SKTypeface)this.GetValue(TypefaceProperty);
set => this.SetValue(TypefaceProperty, value);
}

public Color ForegroundColor
{
get => (Color)this.GetValue(ForegroundColorProperty);
set => this.SetValue(ForegroundColorProperty, value);
}

public Color BackgroundColor
{
get => (Color)this.GetValue(BackgroundColorProperty);
set => this.SetValue(BackgroundColorProperty, value);
}

public double BackgroundCornerRadius
{
get => (double)this.GetValue(BackgroundCornerRadiusProperty);
set => this.SetValue(BackgroundCornerRadiusProperty, value);
}

public SKBitmap ProcessImage(SKBitmap processingImage, ImageProcessingBase imageProcessor)
{
string text = this.WatermarkText;

if (processingImage == null || string.IsNullOrEmpty(text))
{
return processingImage;
}

if (imageProcessor is Watermark watermark)
{
using (SKCanvas canvas = new(processingImage))
using (SKPaint paint = new())
{
paint.IsAntialias = true;
paint.Style = SKPaintStyle.Fill;
paint.TextSize = (float)this.FontSize;
paint.Typeface = this.Typeface ?? PlatformInfo.DefaultTypeface;

paint.EnsureHasValidFont(text);

SKRect measuredText = SKRect.Empty;
paint.MeasureText(text, ref measuredText);
float x = 0f;
float y = 0f;

SKRect rect = new(0f, 0f, processingImage.Width, processingImage.Height);

switch (this.HorizontalWatermarkLocation)
{
case WatermarkLocation.Center:
x = rect.MidX - measuredText.MidX;
break;
case WatermarkLocation.End:
x = rect.Left + rect.Width - measuredText.Width - (float)this.WatermarkPadding;
break;
case WatermarkLocation.Start:
default:
x = rect.Left + (float)this.WatermarkPadding;
break;
}

switch (this.VerticalWatermarkLocation)
{
case WatermarkLocation.Center:
y = rect.MidY - (measuredText.Height / 2f);
break;
case WatermarkLocation.End:
y = rect.Top + rect.Height - (measuredText.Height / 2f) - (float)this.WatermarkPadding;
break;
case WatermarkLocation.Start:
default:
y = rect.Top + (measuredText.Height / 2f) + (float)this.WatermarkPadding;
break;
}

paint.Color = this.BackgroundColor.ToSKColor();
float halfPadding = (float)this.WatermarkPadding / 2f;
canvas.DrawRoundRect(
x - halfPadding, y - (measuredText.Height / 2f) - halfPadding,
measuredText.Width + (float)this.WatermarkPadding, measuredText.Height + (float)this.WatermarkPadding,
(float)this.BackgroundCornerRadius, (float)this.BackgroundCornerRadius,
paint);

paint.Color = this.ForegroundColor.ToSKColor();
canvas.DrawTextCenteredVertically(text, new SKPoint(x, y), paint);

canvas.Flush();

return processingImage;
}
}

return processingImage;
}
}
24 changes: 18 additions & 6 deletions AuroraControlsMaui/Platforms/Android/NumericEntryHandler.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
using System.Collections.Generic;
using System.Globalization;
using System.Text.RegularExpressions;
using Android.Text;
using Android.Text;
using AndroidX.AppCompat.Widget;
using AuroraControls.Platforms.Android;
using Java.Lang;
using Microsoft.Maui.Handlers;

namespace AuroraControls;

public partial class NumericEntryHandler : EntryHandler
public partial class NumericEntryHandler : EntryHandler, IDisposable
{
#nullable enable
private IInputFilter[]? _startingInputFilters;
@@ -83,4 +79,20 @@ protected override void Dispose(bool disposing)
base.Dispose(disposing);
}
}

public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}

protected virtual void Dispose(bool disposing)
{
if (disposing)
{
this._numericInputFilter?.Dispose();
}
}

~NumericEntryHandler() => Dispose(false);
}

0 comments on commit ee5e377

Please sign in to comment.