WindowsCommunityToolkit icon indicating copy to clipboard operation
WindowsCommunityToolkit copied to clipboard

How to get perfect square with ImageCropper

Open michael-hawker opened this issue 2 years ago • 5 comments

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!

michael-hawker avatar Feb 10 '22 18:02 michael-hawker

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 🙌

ghost avatar Feb 10 '22 18:02 ghost

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)

Arlodotexe avatar Aug 10 '22 21:08 Arlodotexe

@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 avatar Aug 10 '22 22:08 michael-hawker

@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...

Arlodotexe avatar Aug 10 '22 22:08 Arlodotexe

!!!

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;

Arlodotexe avatar Aug 10 '22 22:08 Arlodotexe