WindowsCommunityToolkit
WindowsCommunityToolkit copied to clipboard
How to get perfect square with ImageCropper
Discussed in https://github.com/CommunityToolkit/WindowsCommunityToolkit/discussions/4478
Originally posted by jhariel8 February 7, 2022 Hello,
I'm working on a project in which we need to have an image that is either taken or uploaded by a user cropped to a perfect square. I thought that setting the aspect ratio to 1.0 would accomplish this; however, when I open the picture (a 1920x1080 photo), the crop box fits to the image. When I shrink the crop box, it makes an elongated rectangle instead of a square. Is there a way to make the crop box a rectangle and keep it in that shape?
For reference, here is my XAML:
`<Grid x:Class="NID.Client.UWP.Views.ImageEditViewNativeUWP" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:NID.Client.UWP.Views" xmlns:i18n="using:NID.Client.Core.Views" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:controls="using:Microsoft.Toolkit.Uwp.UI.Controls" xmlns:xam="using:Xamarin.Forms" mc:Ignorable="d" x:Name="mainGrid">
<Grid.RowDefinitions>
<RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="70"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"></ColumnDefinition>
<ColumnDefinition Width="600"></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<controls:ImageCropper x:Name="imgCropper" Grid.Row="1" Grid.Column="1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Height="{Binding Path=CROPPER_MAX_HEIGHT, ElementName=mainGrid}"></controls:ImageCropper>
<Grid Grid.Row="2" Grid.ColumnSpan="3" Margin="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"></ColumnDefinition>
<ColumnDefinition Width=".8*"></ColumnDefinition>
<ColumnDefinition Width=".8*"></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Button x:Name="Cancel" Grid.Column="1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="10" Content="{x:Bind Path=i18n:Translator.Instance.GetValue('Cancel'), Mode=OneWay}" Style="{StaticResource BaseButtonUWP}"></Button>
<Button x:Name="Done" Grid.Column="2" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="10" Content="{x:Bind Path=i18n:Translator.Instance.GetValue('Done'), Mode=OneWay}" Style="{StaticResource BaseButtonUWP}"></Button>
</Grid>
</Grid>`
The aspect ratio is set in the code-behind to 1.0.
I would appreciate any thoughts or suggestions!
Hello michael-hawker, thank you for opening an issue with us!
I have automatically added a "needs triage" label to help get things started. Our team will analyze and investigate the issue, and escalate it to the relevant team if possible. Other community members may also look into the issue and provide feedback 🙌
I'll be taking over this issue from here.
I spent some time setting up a minimal repro, and wasn't able to get it to produce the bug. Going back to the original discussion, I found a repro from the original author.
I'll be updating this comment with my findings as I go.
Looks like the source of the problem is that calling await imgCropper.LoadImageFromFile(file);
doesn't wait for the image to load before completing.
var file = await StorageFile.GetFileFromApplicationUriAsync(new Uri("ms-appx:///Assets\\Screenshot 2021-03-16 083652.png"));
await imgCropper.LoadImageFromFile(file);
this.imgCropper.AspectRatio = 1.0;
Adding a Task.Delay()
with any value (minimum of 1) fixes the issue.
Notably, this issue also exists when do you
imgCropper.Source = writableBitmap;
imgCropper.AspectRatio = 1;
Additional information
- When setting the Source property, CanvasRect has a width but no height, causing it to be treated as invalid.
- Presumably, this is because the image is not yet loaded and the containing Grid row uses
Auto
for a height. - Assigning a
Height
to the ImageCropper is a workaround (not a fix)
- Presumably, this is because the image is not yet loaded and the containing Grid row uses
@Arlodotexe the LoadImageFromFile function seems to be awaiting the setsource here:
https://github.com/CommunityToolkit/WindowsCommunityToolkit/blob/7a9bf31975a084338b586d7fdfe3b1e077c8da33/Microsoft.Toolkit.Uwp.UI.Controls.Media/ImageCropper/ImageCropper.cs#L373-L382
Basically there's either something that's happening in the XAML system for image loading or our Source changed callback here:
https://github.com/CommunityToolkit/WindowsCommunityToolkit/blob/7a9bf31975a084338b586d7fdfe3b1e077c8da33/Microsoft.Toolkit.Uwp.UI.Controls.Media/ImageCropper/ImageCropper.Properties.cs#L33-L48
Which we also need to wait on as well, eh?
Do we just need to use a TaskCompletionSource
in the LoadImageFromFile method and register to the SourcePropertyChanged, so that we can complete the awaited task for that after that code has run? Then we can of course unregister and continue on?
Would that be sufficient?
@michael-hawker I did try adding this code in the OnSourceChanged
method:
var taskCompletionSource = new TaskCompletionSource<object>();
target._sourceImage.ImageOpened += OnImageOpenedOrFailed;
target._sourceImage.ImageFailed += OnImageOpenedOrFailed;
await taskCompletionSource.Task;
target._sourceImage.ImageOpened -= OnImageOpenedOrFailed;
target._sourceImage.ImageFailed -= OnImageOpenedOrFailed;
void OnImageOpenedOrFailed(object sender, RoutedEventArgs e) => taskCompletionSource.SetResult(null);
Which caused some fun side effects that we don't want.
I think the root of the issue is definitely that the AspectRatio is being set while the image is still being loaded. There's a lot of code in UpdateAspectRatio()
that is getting skipped and probably not called again once the image is loaded. Investigating if that can help...
!!!
That was it. Calling UpdateAspectRatio(true);
inside of ImageCanvas_SizeChanged
fixes the issue, that's all it took 🚀
Doing this fixes it for both of these scenarios that were broken:
var file = await StorageFile.GetFileFromApplicationUriAsync(new Uri("ms-appx:///Assets\\Screenshot 2021-03-16 083652.png"));
await imgCropper.LoadImageFromFile(file);
this.imgCropper.AspectRatio = 1.0;
var url = new Uri("http://ipfs.io/ipfs/QmbnXcqg7s5J6wsrP85VkiUdfFnYvpfPGZYRkQ5CteA1BG");
var stream = await RandomAccessStreamReference.CreateFromUri(url).OpenReadAsync();
var writableBitmap = new WriteableBitmap(1000, 1000);
await writableBitmap.SetSourceAsync(stream);
imgCropper.Source = writableBitmap;
imgCropper.AspectRatio = 1;