Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Custom image cropping in Windows Phone 7 - Part 2 of 2

DZone's Guide to

Custom image cropping in Windows Phone 7 - Part 2 of 2

· Mobile Zone
Free Resource

Launching an app doesn’t need to be daunting. Whether you’re just getting started or need a refresher on mobile app testing best practices, this guide is your resource! Brought to you in partnership with Perfecto

In my previous article I set the ground for a custom cropping mechanism. To be specific, I set the main UI layout and showed how it is possible to open an image to be cropped. But that's not it yet - now you actually have to perform the cropping. And this process comes with specific interesting aspects that should be considered.

For now, we have the ManipulationDelta handler that allows us to resize the cropping area. It is time to make it move now. For this purpose, I introduced a Boolean variable in the main class that will show whether the cropping box is in move mode or not.

bool isMove = false;

Now, when the translation is performed, the translation values on X and Y axis should be temporarily stored to be manipulated. Therefore, in the class header I am adding two fields:

int trX = 0;
int trY = 0;

Also, I will need to keep track of the rectangle that defines the cropping region, so there is a Rectangle instance defined:

Rectangle r;

So before going any further, I need to adjust the size of LayoutRoot - the main grid where the image is placed, so that I can reference the Rectangle inside the Grid, therefore using the same coordinates on top of the image. To do this, in the existing SetPicture method add two lines at the end:

LayoutRoot.Height = image1.Height;
LayoutRoot.Width = image1.Width;

Let's now proceed to the easiest part - button switching. Obviously, when the user clicks on Move, then the Resize mode is disabled and vice versa. Here is what I have for this:

private void btn_Click(object sender, EventArgs e)
{
IApplicationBarIconButton button;
if (isMove)
{
button = (IApplicationBarIconButton)ApplicationBar.Buttons[1];
button.IsEnabled = true;
button = (IApplicationBarIconButton)ApplicationBar.Buttons[0];
button.IsEnabled = false;
isMove = false;
}
else
{
button = (IApplicationBarIconButton)ApplicationBar.Buttons[1];
button.IsEnabled = false;
button = (IApplicationBarIconButton)ApplicationBar.Buttons[0];
button.IsEnabled = true;
isMove = true;
}
}

Look at the XAML I have for the application buttons (since that is what I am using as the "switch panel"):

<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar IsVisible="True" IsMenuEnabled="True">
<shell:ApplicationBarIconButton IconUri="/Images/appbar.download.rest.png" Text="Resize" x:Name="btnResize" Click="btn_Click" IsEnabled="False"/>
<shell:ApplicationBarIconButton IconUri="/Images/appbar.upload.rest.png" Text="Move" x:Name="btnMove" Click="btn_Click" IsEnabled="True"/>
<shell:ApplicationBarIconButton IconUri="/Images/appbar.check.rest.png" Text="Accept" Click="Accept_Click" />
<shell:ApplicationBarIconButton IconUri="/Images/appbar.close.rest.png" Text="Cancel" />
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>

Notice one interesting fact - I am sharing the Click event handler between the two mode buttons. In Windows Phone 7 application buttons aren't referenced on a name basis, but rather on an index one. Therefore, in the event handler itself I have to detect what button was clicked by checking the isMove field, which is only set by these buttons. Confusing a bit? Don't worry about that - you'll get used to it.

There is nothing special about the event handler itself - all that is done there is enabling and disabling buttons at the same time setting the value of isMove (as you remember, it shows whether the Move or Resize mode is on).

The ManipulationDelta event handler went through a small overhaul here. Here it is:

void rect_ManipulationDelta(object sender, ManipulationDeltaEventArgs e)
{
    GeneralTransform gt = ((Rectangle)sender).TransformToVisual(LayoutRoot);
    Point p = gt.Transform(new Point(0, 0));

    int intermediateValueY = (int)((LayoutRoot.Height - ((Rectangle)sender).Height));
    int intermediateValueX = (int)((LayoutRoot.Width - ((Rectangle)sender).Width));
    Rectangle croppingRectangle = (Rectangle)sender;

    if (isMove)
    {
        TranslateTransform tr = new TranslateTransform();
        trX += (int)e.DeltaManipulation.Translation.X;
        trY += (int)e.DeltaManipulation.Translation.Y;

        if (trY < (-intermediateValueY /2))
        {
            trY = (-intermediateValueY /2);
        }
        else if (trY > (intermediateValueY/2))
        {
            trY = (intermediateValueY/2);
        }

        if (trX < (-intermediateValueX/2))
        {
            trX = (-intermediateValueX/2);
        }
        else if (trX > (intermediateValueX/2))
        {
            trX = (intermediateValueX/2);
        }

        tr.X = trX;
        tr.Y = trY;

        croppingRectangle.RenderTransform = tr;
    }
    else
    {
        if (p.X >= 0)
        {
            if (p.X <= intermediateValueX)
            {
                croppingRectangle.Width -= (int)e.DeltaManipulation.Translation.X;
            }
            else
            {
                croppingRectangle.Width -= (p.X - intermediateValueX);
            }
        }
        else
        {
            croppingRectangle.Width -= Math.Abs(p.X);
        }

        if (p.Y >= 0)
        {
            if (p.Y <= intermediateValueY)
            {
                croppingRectangle.Height -= (int)e.DeltaManipulation.Translation.Y;
            }
            else
            {
                croppingRectangle.Height -= (p.Y - intermediateValueY);
            }
        }
        else
        {
            croppingRectangle.Height -= Math.Abs(p.Y);
        }
    }
}

That's a lot of code to start with. But let me break it down, since the pieces are really quite simple.

First of all, I need to find the cropping rectangle position inside the Grid (in my case, LayoutRoot). Unlike a WinForms application, a WP7 application doesn't really let you find the control coordinates inside another control. Or, at least, it takes a bit more effort to do this. And this snippet does it:

GeneralTransform gt = ((Rectangle)sender).TransformToVisual(LayoutRoot);
Point p = gt.Transform(new Point(0, 0));

Most of the location and size processing is done with the help of an intermediate value.

int intermediateValueY = (int)((LayoutRoot.Height - ((Rectangle)sender).Height));
int intermediateValueX = (int)((LayoutRoot.Width - ((Rectangle)sender).Width));

Both for height (intermediateValueY) and width (intermediateValueX), the intermediate value is the difference between the height/width of the main grid (and therefore, the image) and the height/width of the cropping rectangle.

Depending on the current mode, I am using translation and rectangle resizing.

if (isMove)
{
TranslateTransform tr = new TranslateTransform();
trX += (int)e.DeltaManipulation.Translation.X;
trY += (int)e.DeltaManipulation.Translation.Y;

if (trY < (-intermediateValueY /2))
{
trY = (-intermediateValueY /2);
}
else if (trY > (intermediateValueY/2))
{
trY = (intermediateValueY/2);
}

if (trX < (-intermediateValueX/2))
{
trX = (-intermediateValueX/2);
}
else if (trX > (intermediateValueX/2))
{
trX = (intermediateValueX/2);
}

tr.X = trX;
tr.Y = trY;

croppingRectangle.RenderTransform = tr;
}
else
{
if (p.X >= 0)
{
if (p.X <= intermediateValueX)
{
croppingRectangle.Width -= (int)e.DeltaManipulation.Translation.X;
}
else
{
croppingRectangle.Width -= (p.X - intermediateValueX);
}
}
else
{
croppingRectangle.Width -= Math.Abs(p.X);
}

if (p.Y >= 0)
{
if (p.Y <= intermediateValueY)
{
croppingRectangle.Height -= (int)e.DeltaManipulation.Translation.Y;
}
else
{
croppingRectangle.Height -= (p.Y - intermediateValueY);
}
}
else
{
croppingRectangle.Height -= Math.Abs(p.Y);
}
}

When I am moving it, all I have to do is make sure that the rectangle is still in the bounds of the grid. Speaking of which, another way to move the rectangle is by using matrices (like it was done in one of Charles Petzold's samples) but TranslateTransform does pretty much the same thing here, so there is no need in a direct transformation layer.

It is a bit more complicated when it comes to resizing the rectangle. Its maximum size is pre-set to be the size of the image, but the bounds aren't defined and that is exactly why I have to check the X and Y coordinates when the rectangle is resized. Those can be negative (automatically means that it is out of bounds), bigger than the intermediate value (also means that it is out of bounds) or in between (means that the rectangle is in "good standing").

Once the rectangle is correctly positioned and has the right size, you have to actually crop it. To do this, I am using the Accept button. The event handler tied to it looks like this:

private void Accept_Click(object sender, EventArgs e)
{
ClipImage();

WriteBitmap(LayoutRoot);

var image = new BitmapImage();
using (var local = new IsolatedStorageFileStream("myImage.jpg", FileMode.Open, IsolatedStorageFile.GetUserStoreForApplication()))
{
image.SetSource(local);
}

WriteDummyImage(image);
}

Here, ClipImage will actually select the needed region (defined by the rectangle) and will transform the image so that the selected part is moved to the left topmost corner of the grid:

void ClipImage()
{
RectangleGeometry geo = new RectangleGeometry();

r = (Rectangle)(from c in LayoutRoot.Children where c.Opacity == .5 select c).First();
GeneralTransform gt = r.TransformToVisual(LayoutRoot);
Point p = gt.Transform(new Point(0, 0));
geo.Rect = new Rect(p.X, p.Y, r.Width, r.Height);
image1.Clip = geo;
r.Visibility = System.Windows.Visibility.Collapsed;

TranslateTransform t = new TranslateTransform();
t.X = -p.X;
t.Y = -p.Y;
image1.RenderTransform = t;
}

Once the selected region is positioned, I am writing the "screenshot" of the grid to a file. I can also try and get just the Image control representation, but this comes with a few strings attached. The image isn't actually cropped inside the Image control - there is a mask applied that leaves the selected region visible. The image remains the same though and the control size doesn't change, so getting the graphical image of the control itself won't be a good thing since I will have to handle lots of whitespace.

Also, due to security restrictions I cannot directly extract the image from an Image control, so I have to have this workaround.

WriteBitmap will store the "screenshot" in the isolated storage for further processing:

void WriteBitmap(FrameworkElement element)
{
WriteableBitmap wBitmap = new WriteableBitmap(element, null);

using (MemoryStream stream = new MemoryStream())
{
wBitmap.SaveJpeg(stream, (int)element.Width, (int)element.Height, 0, 100);

using (var local = new IsolatedStorageFileStream("myImage.jpg", FileMode.Create, IsolatedStorageFile.GetUserStoreForApplication()))
{
local.Write(stream.GetBuffer(), 0, stream.GetBuffer().Length);
}
}
}

You can change the name, obviously and if you plan to implement a custom control on top of this mechanism, you shouldn't be using any pre-set file names in the first place, since those can be conflicting with user content. But that's a separate discussion.

Once the grid representation is saved, I am loading it again and now I am calling WriteDummyImage with the opened image passed as a parameter:

private void WriteDummyImage(BitmapImage image)
{
Image imageC = new Image();
imageC.Stretch = Stretch.None;
imageC.Source = image;
imageC.Height = r.Height;
imageC.Width = r.Width;

WriteBitmap(imageC);
}

I am creating a new image that will allow me to select only the needed part - now that it is a JPEG and has no whitespace around it. I am adjusting the horizontal and vertical margins simply by changing the size of the image to the size of the cropping rectangle, and then once again I am writing the image contents to a file in the isolated storage.

The cropped image is now there and you are able to access it anytime!

On a side note, I did not put restrictions on the minimum size of the cropping rectangle due to the fact that some regions require very small sections to be selected, therefore you need to set those by yourself if you have a minimum crropping region size limit. This can be done in ManipulationDelta. Also, the cropped image is dependent on the actual size of the Image control, not the image itself. Play around with the Stretch mode if you need different cropping sizes.

You can download the sample project I was working on here.

Keep up with the latest DevTest Jargon with the latest Mobile DevTest Dictionary. Brought to you in partnership with Perfecto.

Topics:

Opinions expressed by DZone contributors are their own.

THE DZONE NEWSLETTER

Dev Resources & Solutions Straight to Your Inbox

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.

X

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}