Freigeben über


Porting a photograph sharing app from iOS to Windows 8

You can never have too many photograph sharing apps (so it appears), and so I wrote my own for the iPhone. I wanted an app with the following features:

  1. Be extremely easy to use,
  2. Allow the user to take a picture,
  3. Optionally apply a preset filter,
  4. Share or save the picture.

I wanted to write a Windows 8 version too. Windows tablets are getting lighter, smaller and more fun - and they have cameras!

The iPhone Version

I wanted the UI of my app to be minimal, displaying the filtering options in a grid and providing only the most basic controls. Get in, take a picture, share it, get out – that was the goal.

Here was my design sketch for the app.

It looked ok, so I fired up Xcode and got to work. Here are some screenshots of the finished app.

 

The app uses the default camera control to allow the user to take a photograph.

 

Once taken, the camera control lets you discard the image and take another one, or use the captured image.

 

The app takes this image, applies 8 filters and allows you to pick one (or tap the middle square, which is unfiltered):

 

When you pick one, the image is displayed a bit larger, along with the share, save and retake buttons.

 

Technical details

 

As a simple app with only three views (the large image/buttons, the camera control, the filter grid) I elected to use a Storyboard layout and (of course) Objective-C. Here are the only two views: the first has three buttons, and an image. The second has a grid of nine images (each overlaid with an invisible button), and a re-shoot button.

In my first attempt, I created a third UIViewController solely to display the camera controls (the UIImagePickerController view). This was a mistake – the camera control is a modal display, conjured with a presentViewController method. However, the Storyboard view I was using was also modal. If there is one thing which iOS seems to hate, it’s presenting a modal view from within another modal view. I got a zillion low memory errors and crashes until I realized what I was doing.

Instead, the Storyboard only has two views: the master view with the single large image and controls, and the grid view. I then made sure the UIImagePickerController was called directly from the master view, and not invoked from another modally presented view.

Once the user has captured a picture, it’s time to do some image processing. iOS 5 introduced CIFilter, which is an object that can apply one or more filters to an UIImage (actually, a CIImage object, which can be easily made from a UIImage). This was perfect for my needs. The only catch was CIFilter’s habit of rotating an image 90’, but by storing the original image orientation before filtering and re-applying it afterwards, I was able to take care of that. I picked eight different filters and set some default values, and that was it: my grid of filter options was done.

To share the image with other apps, I just needed to call the UIActivityViewController and provide some text and link to the image. The app was complete.

 

The Windows 8 Version

For the Windows 8 version, I decided to go with C# and XAML. I picked C# over JavaScript, as although I do like using JavaScript,  I was thinking of maybe porting the app to Windows Phone, and the phone doesn't support JavaScript as a native development language.  I also considered C++ for a while, but for a simple app like this, C# is quicker to develop in and the extra speed of C++ just isn't required.

The larger screen on a typical Windows tablet meant I didn't have to consider using multiple pages to display the user interface  - here’s my sketch. Notice the attention to detail (not). I just scribbled out a few sketches to see what might work. I knew I was going to play around in Blend to try a few things too, so I didn't spend a lot of time in this stage.

 

In order to make things easier as I switched between the several computers I was using for development (an ASUS Vivotab Windows tablet, and a Mac Pro running Windows 8 under Parallels), I decided to do things properly and store my source code in a source control environment.  I often use Dropbox or Skydrive to share projects between machines, but Visual Studio doesn't seem to like this very much, and although I use the optional version history feature of Dropbox, it’s far from an sophisticated solution for software development.

So I signed up for a free Team Foundation Service account on VisualStudio.com. I really love this service (I mean, it’s currently free for one thing, so you can't go wrong with that) and it’s a piece of cake to use. Once you set up a project, checking in your source code or updating to the latest version is a single click: 

Even if you never plan on using the more advanced features or bug tracking, and even if you are the only one working on your project, it’s still a great way to make sure you can get access to your code no matter what machine you’re on.

I started with the blank C#/XAML project, and created the ten Image objects, and three Button objects that my app required. I then switched to Blend to resize and position everything until I was happy. I made sure to group related components together in StackPanel objects: this keeps them together when the screen size changes. All the controls are in one top level Grid control – this control maximizes the app's size, making sure as much screen is used as possible. The end result is that the app looks good on the smallest Windows 8 devices available, as well as larger monitors: all without me having to do anything about it.

The Windows equivalent of UIImagePickerController is the CameraCaptureUI. As on iOS, this control handles everything camera-related for us, and when it is closed, we’re left with an image. We do have some work to do in order to extract an actual image out of the control and massage it into the most suitable format. A lot of these operations are performed asynchronously, which is great for keeping the UI feeling fluid but it does mean you need to work with async/await which is something that may be new to you. It’s not a big deal though. Remember to edit your app's project manifest file to get camera access!

Again, once the image is captured, it’s time to process it. Sadly we don’t have CIFilter to do the work for us (the nearest is Direct2D's effects abilities), so I wrote some code that lets me access the raw bytes that make up the colors of each pixel in each image. Using this awesome topic as reference, it was fun to go back to some maths to tweak the colors. I started with a simple gamma correction algorithm, which I then adapted to use look-up tables as there were a lot of POW() functions being called. Then I just messed with the RGB values to make interesting changes.

Artwork

I created the icon and splash screen designs by taking some of the icons I’d bought from Glyphish and doing some Photoshop to them. When working between different iOS versions and Windows releases, it’s getting to be a real pain keeping track of all the different sizes of icons. For completely original artwork, I’d definitely try to keep everything in vector format until the last second – and exporting PNGs only when I was done. By the way, the Windows 8 emulator also allows you to capture screen shots as you run the app, ideal for creating images for uploading to the store when you submit your app. And most impressive of all, the emulator also supports the host device's webcam.

Look what I made

When completed, the Windows app looked like this. The app starts with the camera control active:

Once a picture is taken, you can tap on a filtered image and then retake the photo, save or share it.

 

The share button actually triggers opening the Share Charm programmatically, like this:

 

private void Button_Click_Share(object sender, RoutedEventArgs e)

        {

            // Open the Share charm, programmatically

            Windows.ApplicationModel.DataTransfer.DataTransferManager.ShowShareUI();

        }

 

Depending on what apps you have installed, you’ll see options to share to Twitter, Facebook and so on. I have to admit, sharing was a little trickier than I expected. In order to support sharing, your app needs to support a callback method and then provide the sharing data in the right format. Sharing images means encoding the raw bitmap into a PNG or JPG, which is – you guessed it – an async operation. This means you need some magic in the sharing callback  to keep the system happy as you process the image. Once you've done it once, you'll be able to do it for any other app.

Certification: FAILED

 The first time I submitted the app to the Windows Store, it failed certification – for several reasons.

  1. I had set the app to require internet access, but I’d no privacy notice text available,
  2. The age rating was set too low for an app that supports sharing information( i.e. pictures),
  3. The app crashed.

The first point was an oversight – I didn't need internet access at all. Any apps that share might, but not this app. The second fail was a matter of bumping the age limit to 12. The final point made me think. I could not crash the app myself, and I couldn't work out why the stack dump which the testers provided could even happen.

Finally it dawned on me that the reviewer was opening the charms and selecting Share before even taking a picture. As a result, the sharing support callback was being called, but no image was ready. Bang! The app crashed. I was able to quickly code around this, resubmit every thing, and all was well. In fact, if you want to go to the Windows Store and download the app now, you can. It's called Clickster. Here's the link. Don't worry, it's free.

Windows 8 C#/XAML Source Code

 

I’m including the XAML and C# source code in case you find it useful Some of the bitmap stuff did have me scratching my head for a bit. The XAML looks large because the buttons are animated slightly, but overall it's a small app. Most of the information I required to write this app was already in the Windows docs, and the little that wasn't is something we're adding in the very near future.

 

If you have any questions, just let me know!

 

John

 

 

 // C# Code

 using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Navigation;
using Windows.Media.Capture;
using Windows.Storage;
using Windows.UI.Xaml.Media.Imaging;
using Windows.Storage.Streams;
using System.Diagnostics;
using Windows.ApplicationModel.DataTransfer;
using Windows.Graphics.Imaging;
using Windows.ApplicationModel;
using Windows.Storage.Pickers;
using Windows.UI.Xaml.Media.Animation;


namespace Clickster
{
    /// <summary>
    /// An empty page that can be used on its own or navigated to within a Frame.
    /// </summary>
    public sealed partial class MainPage : Page
    {

        private DataTransferManager dataTransferManager; // Used when sharing
        private bool bitmapsCreated = false;
        private WriteableBitmap[] bitmaps;
        private bool takenAtLeastOne = false;
        private int height, width;
        private byte[] glut15, glut05, glut35;

        public MainPage()
        {
            this.InitializeComponent();
            CameraCaptureUI_Click();
            dataTransferManager = DataTransferManager.GetForCurrentView();
            dataTransferManager.DataRequested += new TypedEventHandler<DataTransferManager, DataRequestedEventArgs>(this.ShareImageHandler);
            generateGluts();

        }


        private async void ShareImageHandler(DataTransferManager sender, DataRequestedEventArgs e)
        {
            //
            // Code require to handle the share request
            //

            DataRequest request = e.Request;
            request.Data.Properties.Title = "Share picture";
            request.Data.Properties.Description = "Here's a picture!";


            if (!takenAtLeastOne)
            {
                request.Data.Properties.Title = "Nothing to share yet! ";
                request.Data.Properties.Description = "Take a picture first.";
            }



            DataRequestDeferral deferral = request.GetDeferral();

            try
            {
                StorageFile thumbnailFile = await Package.Current.InstalledLocation.GetFileAsync("Assets\\Logo100.PNG");
                request.Data.Properties.Thumbnail = RandomAccessStreamReference.CreateFromFile(thumbnailFile);

                if (!takenAtLeastOne)
                {
                    request.Data.SetBitmap(RandomAccessStreamReference.CreateFromFile(thumbnailFile));
                }
                else
                {

                    using (var stream = new InMemoryRandomAccessStream())
                    {
                        var encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, stream);

                        var pixelStream = bitmaps[0].PixelBuffer.AsStream();
                        byte[] pixels = new byte[pixelStream.Length];

                        await pixelStream.ReadAsync(pixels, 0, pixels.Length);

                        encoder.SetPixelData(BitmapPixelFormat.Bgra8, BitmapAlphaMode.Ignore, (uint)width, (uint)height, 96.0, 96.0, pixels);
                        await encoder.FlushAsync();

                        request.Data.SetBitmap(RandomAccessStreamReference.CreateFromStream(stream));
                    }
                }
            }
            finally
            {
                deferral.Complete();
            }
        
        }
        

        private byte Sum(int b, int d)
        {
            b = b + d;

            if (b < 0) return 0;
            if (b > 255) return 255;

            return (byte)b;

        }

        private byte Product(float b, float d)
        {
            b = b * d;

            if (b < 0) return 0;
            if (b > 255) return 255;

            return (byte)b;

        }





        private void generateGluts()
        {
            glut15 = new byte[256];
            glut05 = new byte[256];
            glut35 = new byte[256];

            for (int i = 0; i < 256; i++)
            {
                glut15[i] = (byte)(255 * (Math.Pow(i / (double)255, 1.5)));
                glut05[i] = (byte)(255 * (Math.Pow(i / (double)255, 0.5)));
                glut35[i] = (byte)(255 * (Math.Pow(i / (double)255, 3.5)));
            }

        }

        private byte Gamma35(byte b)
        {
            return glut35[b];
        }

        private byte Gamma15(byte b)
        {
            return glut15[b];
        }

        private byte Gamma05(byte b)
        {
            return glut05[b];
        }

        private byte Gamma(byte b, float g)
        {
            double d = (double)b;

            return (byte)(255 * (Math.Pow(d / (double)255, g)));

        }


        private void BitmapProcess(int from, int to, int mode)
        {

            using (var ibuffer = bitmaps[from].PixelBuffer.AsStream())
            {
                using (var obuffer = bitmaps[to].PixelBuffer.AsStream())
                {

                    byte newB = 0;
                    byte newG = 0;
                    byte newR = 0;


                    Byte[] pixels = new Byte[4 * width * height];
                    ibuffer.Read(pixels, 0, pixels.Length);

                    for (int x = 0; x < width; x++)
                    {

                        for (int y = 0; y < height; y++)
                        {


                            int index = ((y * width) + x) * 4;

                            Byte b = pixels[index + 0];
                            Byte g = pixels[index + 1];
                            Byte r = pixels[index + 2];
                            Byte a = pixels[index + 3];

                            switch (mode)
                            {
                                case 1:
                                    // Gamma increase
                                    newB = Gamma15(b);
                                    newG = Gamma15(g);
                                    newR = Gamma15(r);

                                    break;

                                case 2:
                                    // Gamma decrease
                                    newB = Gamma05(b);
                                    newG = Gamma05(g);
                                    newR = Gamma05(r);
                                    break;

                                case 3:
                                    // Redder
                                    newB = Gamma15((Byte)((r + g + b) / 3));
                                    newG = Gamma35((Byte)((r + g + b) / 3));
                                    newR = Gamma05((Byte)((r + g + b) / 3));
                                    break;

                                case 4:
                                    // Less Red
                                    newB = Gamma15((Byte)((r + g + b) / 3));
                                    newG = Gamma05((Byte)((r + g + b) / 3));
                                    newR = Gamma35((Byte)((r + g + b) / 3));
                                    break;

                                case 5:
                                    // Bluer
                                    newB = Product((float)b, 1.3f);
                                    newG = Product((float)g, 0.8f);
                                    newR = Product((float)r + (255 - g), 0.8f);

                                    break;

                                case 6:
                                    // Less Blue
                                    newB = Gamma05((Byte)((r + r + b) / 3));
                                    newG = Gamma15((Byte)((r + g + b) / 3));
                                    newR = Gamma05((Byte)((r + b + b) / 3));

                                    break;


                                case 7:
                                    // Mono
                                    newB = (Byte)((b + r + g) / 3);
                                    newG = (Byte)((b + r + g) / 3);
                                    newR = (Byte)((r + g + b) / 3);



                                    break;

                                case 8:
                                    // intense mono
                                    newB = Gamma15((Byte)((r + g + b) / 3));
                                    newG = Gamma15((Byte)((r + g + b) / 3));
                                    newR = Gamma15((Byte)((r + g + b) / 3));

                                    break;

                            }

                            pixels[index + 0] = newB;
                            pixels[index + 1] = newR;
                            pixels[index + 2] = newG;
                            pixels[index + 3] = a;
                        }
                    }

                    obuffer.Position = 0;
                    obuffer.Write(pixels, 0, pixels.Length);
                }
            }
        }




        private void ProcessAllBitmaps()
        {

            originalImage.Source = bitmaps[0];

            FadeInImage.Stop();
            FadeInImage.SetValue(Storyboard.TargetNameProperty, "final");
            FadeInImage.Begin();


            for (int i = 1; i < 9; i++)
            {
                bitmaps[i].Invalidate();
                BitmapProcess(0, i, i);

                switch (i)
                {

                    case 1: option1Image.Source = bitmaps[1]; imageAppearsAnimation1.Begin(); break;
                    case 2: option2Image.Source = bitmaps[2]; imageAppearsAnimation2.Begin(); break;
                    case 3: option3Image.Source = bitmaps[3]; imageAppearsAnimation3.Begin(); break;
                    case 4: option4Image.Source = bitmaps[4]; imageAppearsAnimation4.Begin(); break;
                    case 5: option5Image.Source = bitmaps[5]; imageAppearsAnimation5.Begin(); break;
                    case 6: option6Image.Source = bitmaps[6]; imageAppearsAnimation6.Begin(); break;
                    case 7: option7Image.Source = bitmaps[7]; imageAppearsAnimation7.Begin(); break;
                    case 8: option8Image.Source = bitmaps[8]; imageAppearsAnimation8.Begin(); break;
                }


            }
        }


        async private void copyBitmap(int from, int to)
        {
            //  A helper function to copy from one bitmap to another

            using (var source = bitmaps[from].PixelBuffer.AsStream())
            {
                using (var destination = bitmaps[to].PixelBuffer.AsStream())
                {
                    await source.CopyToAsync(destination);
                }
            }
        }

        async private void CameraCaptureUI_Click()
        {

            // Code to use the camera capure UI

            //
            // Remember to set permissions in the manifest!
            //

            retakeButton.Visibility = Visibility.Collapsed;
            saveButton.Visibility = Visibility.Collapsed;
            shareButton.Visibility = Visibility.Collapsed;

            CameraCaptureUI cameraUI = new CameraCaptureUI();

            cameraUI.PhotoSettings.AllowCropping = false;

            cameraUI.PhotoSettings.MaxResolution = CameraCaptureUIMaxPhotoResolution.MediumXga;

            Windows.Storage.StorageFile capturedMedia =
                await cameraUI.CaptureFileAsync(CameraCaptureUIMode.Photo);

            if (capturedMedia != null)
            {
                using (var stream2 = await capturedMedia.OpenAsync(FileAccessMode.Read))
                {

                    Windows.UI.Xaml.Media.Imaging.BitmapImage bitmapCamera = new Windows.UI.Xaml.Media.Imaging.BitmapImage();
                    bitmapCamera.SetSource(stream2);
                    originalImage.Source = bitmapCamera;

                    if (!bitmapsCreated)
                    {
                        bitmapsCreated = true;

                        bitmaps = new WriteableBitmap[9];

                        width = bitmapCamera.PixelWidth;
                        height = bitmapCamera.PixelHeight;

                        for (int i = 0; i < 9; i++)
                        {
                            bitmaps[i] = new WriteableBitmap(width, height);
                        }
                    }
                }

                using (var stream = await capturedMedia.OpenAsync(FileAccessMode.Read))
                {
                    bitmaps[0].SetSource(stream);
                }

                final.Source = bitmaps[0];

                ProcessAllBitmaps();

                retakeButton.Visibility = Visibility.Visible;
                saveButton.Visibility = Visibility.Visible;
                shareButton.Visibility = Visibility.Visible;
                takenAtLeastOne = true;
            }
            else
            {
                // No picture was taken - perhaps the user tapped the 'back' button on camera control page.

                retakeButton.Visibility = Visibility.Visible;

                if (takenAtLeastOne)
                {
                    saveButton.Visibility = Visibility.Visible;
                    shareButton.Visibility = Visibility.Visible;
                }


            }

        }




     
        

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            // Retake picture
            CameraCaptureUI_Click();
        }


        private async void Button_SavePicture(object sender, RoutedEventArgs e)
        {
            // Save the image to disk

            FileSavePicker picker = new FileSavePicker();
            picker.FileTypeChoices.Add("JPG File", new List<string>() { ".jpg" });
            StorageFile file = await picker.PickSaveFileAsync();

            if (file != null)
            {
                using (IRandomAccessStream stream = await file.OpenAsync(FileAccessMode.ReadWrite))
                {
                    BitmapEncoder encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.JpegEncoderId, stream);
                    Stream pixelStream = bitmaps[0].PixelBuffer.AsStream();

                    byte[] pixels = new byte[pixelStream.Length];
                    await pixelStream.ReadAsync(pixels, 0, pixels.Length);
                    encoder.SetPixelData(BitmapPixelFormat.Bgra8, BitmapAlphaMode.Ignore, (uint)width, (uint)height, 96.0, 96.0, pixels);
                    await encoder.FlushAsync();
                }
            }

        }

        void selectButton(int button)
        {
            // Copy the correct bitmap from the filter button when user
            // taps on a filter.

            final.Source = bitmaps[button];
            copyBitmap(button, 0);
        }

        private void optionImage_PointerPressed(object sender, PointerRoutedEventArgs e)
        {
            // All the user to select a filter. Each image has a tag associated with it,
            // so the touch handlers all come here and then the correct filter is used.

            int t = Convert.ToInt16((sender as Image).Tag.ToString());

            TapImage.Stop();
            TapImage.SetValue(Storyboard.TargetNameProperty, (sender as Image).Name);
            TapImage.Begin();

            if (t == 0)
            {
                originalImage.Source = bitmaps[0];
                final.Source = bitmaps[0];

            }
            else
                selectButton(t);

            FadeInImage.Stop();
            FadeInImage.SetValue(Storyboard.TargetNameProperty, "final");
            FadeInImage.Begin();

        }


        private void Button_Click_Share(object sender, RoutedEventArgs e)
        {
            // Open the Share charm, programmatically
            Windows.ApplicationModel.DataTransfer.DataTransferManager.ShowShareUI();
        }
    }
}

 

 // XAML Code

 

 <Page
    x:Class="Clickster.MainPage"
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:Clickster"
    xmlns:d="https://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="https://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">


    <Page.Resources>
        <Storyboard x:Name="TapImage">
            <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.ScaleX)" >
                <EasingDoubleKeyFrame KeyTime="0" Value="1"/>
                <EasingDoubleKeyFrame KeyTime="0:0:0.05" Value="0.95"/>
                <EasingDoubleKeyFrame KeyTime="0:0:0.1" Value="1"/>
            </DoubleAnimationUsingKeyFrames>
            <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.ScaleY)" >
                <EasingDoubleKeyFrame KeyTime="0" Value="1"/>
                <EasingDoubleKeyFrame KeyTime="0:0:0.05" Value="0.95"/>
                <EasingDoubleKeyFrame KeyTime="0:0:0.1" Value="1"/>
            </DoubleAnimationUsingKeyFrames>
        </Storyboard>
        <Storyboard x:Name="FadeInImage">
            <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)">
                <EasingDoubleKeyFrame KeyTime="0" Value="0.5"/>
                <EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="1"/>
            </DoubleAnimationUsingKeyFrames>
        </Storyboard>

        <Storyboard x:Name="imageAppearsAnimation1" TargetName="option1Image">
            <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)" >
                <EasingDoubleKeyFrame KeyTime="0" Value="0"/>
                <EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="1"/>
            </DoubleAnimationUsingKeyFrames>
        </Storyboard>

        <Storyboard x:Name="imageAppearsAnimation2" TargetName="option2Image">
            <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)" >
                <EasingDoubleKeyFrame KeyTime="0" Value="0"/>
                <EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="1"/>
            </DoubleAnimationUsingKeyFrames>
        </Storyboard>
        <Storyboard x:Name="imageAppearsAnimation3" TargetName="option3Image">
            <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)" >
                <EasingDoubleKeyFrame KeyTime="0" Value="0"/>
                <EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="1"/>
            </DoubleAnimationUsingKeyFrames>
        </Storyboard>
        <Storyboard x:Name="imageAppearsAnimation4" TargetName="option4Image">
            <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)" >
                <EasingDoubleKeyFrame KeyTime="0" Value="0"/>
                <EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="1"/>
            </DoubleAnimationUsingKeyFrames>
        </Storyboard>
        <Storyboard x:Name="imageAppearsAnimation5" TargetName="option5Image">
            <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)" >
                <EasingDoubleKeyFrame KeyTime="0" Value="0"/>
                <EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="1"/>
            </DoubleAnimationUsingKeyFrames>
        </Storyboard>
        <Storyboard x:Name="imageAppearsAnimation6" TargetName="option6Image">
            <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)" >
                <EasingDoubleKeyFrame KeyTime="0" Value="0"/>
                <EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="1"/>
            </DoubleAnimationUsingKeyFrames>
        </Storyboard>
        <Storyboard x:Name="imageAppearsAnimation7" TargetName="option7Image">
            <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)" >
                <EasingDoubleKeyFrame KeyTime="0" Value="0"/>
                <EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="1"/>
            </DoubleAnimationUsingKeyFrames>
        </Storyboard>
        <Storyboard x:Name="imageAppearsAnimation8" TargetName="option8Image">
            <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)" >
                <EasingDoubleKeyFrame KeyTime="0" Value="0"/>
                <EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="1"/>
            </DoubleAnimationUsingKeyFrames>
        </Storyboard>





    </Page.Resources>

    <Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}" HorizontalAlignment="Center" VerticalAlignment="Center">
        <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center" Background="Black">
            <StackPanel Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0">

                <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="10,30">
                    <Image x:Name="option1Image" Width="256" Height="160" Tag="1"  PointerPressed="optionImage_PointerPressed" Margin="5,0" RenderTransformOrigin="0.5,0.5">
                        <Image.RenderTransform>
                            <CompositeTransform/>
                        </Image.RenderTransform>
                    </Image>
                    <Image x:Name="option2Image" Width="256" Height="160" Tag="2"  PointerPressed="optionImage_PointerPressed" Margin="5,0" RenderTransformOrigin= "0.5,0.5" >
                        <Image.RenderTransform>
                            <CompositeTransform/>
                        </Image.RenderTransform>
                    </Image>
                    <Image x:Name="option3Image" Width="256" Height="160" Tag="3"  PointerPressed="optionImage_PointerPressed" Margin="5,0" RenderTransformOrigin="0.5,0.5">
                        <Image.RenderTransform>
                            <CompositeTransform/>
                        </Image.RenderTransform>
                    </Image>
                </StackPanel>

                <StackPanel Margin="10,30" VerticalAlignment="Center" Orientation="Horizontal" HorizontalAlignment="Center">

                    <Image x:Name="option4Image" Width="256" Height="160" Tag="4" PointerPressed="optionImage_PointerPressed" Margin="5,0" RenderTransformOrigin="0.5,0.5">
                        <Image.RenderTransform>
                            <CompositeTransform/>
                        </Image.RenderTransform>
                    </Image>
                    <Image x:Name="originalImage" Width="256" Height="160" Tag="0" PointerPressed="optionImage_PointerPressed" Margin="5,0" RenderTransformOrigin="0.5,0.5">
                        <Image.RenderTransform>
                            <CompositeTransform/>
                        </Image.RenderTransform>
                    </Image>
                    <Image x:Name="option5Image" Width="256" Height="160" Tag="5" PointerPressed="optionImage_PointerPressed" Margin="5,0" RenderTransformOrigin="0.5,0.5">
                        <Image.RenderTransform>
                            <CompositeTransform/>
                        </Image.RenderTransform>
                    </Image>
                </StackPanel>

                <StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="10,30" HorizontalAlignment="Center">
                    <Image x:Name="option6Image" Width="256" Height="160" Tag="6" PointerPressed="optionImage_PointerPressed" Margin="5,0" RenderTransformOrigin="0.5,0.5">
                        <Image.RenderTransform>
                            <CompositeTransform/>
                        </Image.RenderTransform>
                    </Image>
                    <Image x:Name="option7Image" Width="256" Height="160" Tag="7" PointerPressed="optionImage_PointerPressed" Margin="5,0" RenderTransformOrigin="0.5,0.5">
                        <Image.RenderTransform>
                            <CompositeTransform/>
                        </Image.RenderTransform>
                    </Image>
                    <Image x:Name="option8Image" Width="256" Height="160" Tag="8" PointerPressed="optionImage_PointerPressed" Margin="5,0" RenderTransformOrigin="0.5,0.5">
                        <Image.RenderTransform>
                            <CompositeTransform/>
                        </Image.RenderTransform>
                    </Image>
                </StackPanel>
            </StackPanel>
            <StackPanel Width="608">
                <Image x:Name="final" Margin="6,27,68,147" Height="387" VerticalAlignment="Top" RenderTransformOrigin="0.5,0.5" Source="Assets/wide.png"  >
                    <Image.RenderTransform>
                        <CompositeTransform/>
                    </Image.RenderTransform>
                </Image>
                <StackPanel Orientation="Vertical" Height="160" Margin="0,-90,0,0" HorizontalAlignment="Center" Width="258" VerticalAlignment="Center">
                    <Button x:Name="retakeButton"  Content="Retake" HorizontalAlignment="Center" VerticalAlignment="Top" Height="52" Click="Button_Click" Margin="0,0,0,3" Width="256" Background="#FFA60101" BorderBrush="#FF590000" FontSize="16"/>
                    <Button x:Name="shareButton"  Content="Share" HorizontalAlignment="Center" VerticalAlignment="Center" Height="52" Margin="0,0,0,3" Width="256" Click="Button_Click_Share" BorderBrush="#FF0B4B01" Background="#FF061B7C" FontSize="16"/>
                    <Button x:Name="saveButton"  Content="Save" HorizontalAlignment="Center" VerticalAlignment="Bottom" Height="52" Margin="0,0,0,3" Width="256" Click="Button_SavePicture" BorderBrush="#FF0B4B01" Background="#FF3B7C06" FontSize="16"/>
                </StackPanel>
            </StackPanel>
        </StackPanel>

    </Grid>
</Page>