├── Stuart
├── Stuart.pfx
├── Stuart_StoreKey.pfx
├── Assets
│ ├── StoreLogo.png
│ ├── SplashScreen.png
│ ├── LockScreenLogo.scale-200.png
│ ├── Square44x44Logo.scale-200.png
│ ├── Wide310x150Logo.scale-200.png
│ ├── Square150x150Logo.scale-200.png
│ └── Square44x44Logo.targetsize-24_altform-unplated.png
├── project.json
├── Properties
│ ├── AssemblyInfo.cs
│ └── Default.rd.xml
├── App.xaml
├── EffectPropertiesControl.xaml
├── Converters.cs
├── EditGroupControl.xaml.cs
├── App.xaml.cs
├── CachedImage.cs
├── Observable.cs
├── Stuart.appxmanifest
├── ExtensionMethods.cs
├── Photo.cs
├── MainPage.xaml
├── Effect.cs
├── EditGroupControl.xaml
├── Stuart.csproj
├── EffectPropertiesControl.xaml.cs
├── EffectMetadata.cs
├── EditGroup.cs
├── MainPage.xaml.cs
└── Package.StoreAssociation.xml
├── Screenshots
├── retro.jpg
├── stylize.jpg
├── adjusted.jpg
├── original.jpg
├── regionEdit.jpg
└── regionSelect.jpg
├── .gitignore
├── PRIVACY.txt
├── LICENSE.txt
├── Stuart.sln
└── README.md
/Stuart/Stuart.pfx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shawnhar/stuart/HEAD/Stuart/Stuart.pfx
--------------------------------------------------------------------------------
/Screenshots/retro.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shawnhar/stuart/HEAD/Screenshots/retro.jpg
--------------------------------------------------------------------------------
/Screenshots/stylize.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shawnhar/stuart/HEAD/Screenshots/stylize.jpg
--------------------------------------------------------------------------------
/Screenshots/adjusted.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shawnhar/stuart/HEAD/Screenshots/adjusted.jpg
--------------------------------------------------------------------------------
/Screenshots/original.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shawnhar/stuart/HEAD/Screenshots/original.jpg
--------------------------------------------------------------------------------
/Screenshots/regionEdit.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shawnhar/stuart/HEAD/Screenshots/regionEdit.jpg
--------------------------------------------------------------------------------
/Stuart/Stuart_StoreKey.pfx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shawnhar/stuart/HEAD/Stuart/Stuart_StoreKey.pfx
--------------------------------------------------------------------------------
/Screenshots/regionSelect.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shawnhar/stuart/HEAD/Screenshots/regionSelect.jpg
--------------------------------------------------------------------------------
/Stuart/Assets/StoreLogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shawnhar/stuart/HEAD/Stuart/Assets/StoreLogo.png
--------------------------------------------------------------------------------
/Stuart/Assets/SplashScreen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shawnhar/stuart/HEAD/Stuart/Assets/SplashScreen.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | obj/
2 | bin/
3 | packages/
4 | AppPackages/
5 | .vs/
6 | *.user
7 | Visual Studio 2015/
8 | project.lock.json
9 |
--------------------------------------------------------------------------------
/Stuart/Assets/LockScreenLogo.scale-200.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shawnhar/stuart/HEAD/Stuart/Assets/LockScreenLogo.scale-200.png
--------------------------------------------------------------------------------
/Stuart/Assets/Square44x44Logo.scale-200.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shawnhar/stuart/HEAD/Stuart/Assets/Square44x44Logo.scale-200.png
--------------------------------------------------------------------------------
/Stuart/Assets/Wide310x150Logo.scale-200.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shawnhar/stuart/HEAD/Stuart/Assets/Wide310x150Logo.scale-200.png
--------------------------------------------------------------------------------
/Stuart/Assets/Square150x150Logo.scale-200.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shawnhar/stuart/HEAD/Stuart/Assets/Square150x150Logo.scale-200.png
--------------------------------------------------------------------------------
/Stuart/Assets/Square44x44Logo.targetsize-24_altform-unplated.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shawnhar/stuart/HEAD/Stuart/Assets/Square44x44Logo.targetsize-24_altform-unplated.png
--------------------------------------------------------------------------------
/PRIVACY.txt:
--------------------------------------------------------------------------------
1 | Stuart does not collect, store or transmit any personal information.
2 |
3 | It accesses images from the user photo collection only when a photograph is explicitly
4 | selected via the "Open" command. Copies of the selected photo are only stored to other
5 | locations if the user explicitly chooses to via the "Save As" command.
6 |
--------------------------------------------------------------------------------
/Stuart/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "Microsoft.NETCore.UniversalWindowsPlatform": "5.0.0",
4 | "Win2D.uwp": "1.18.0"
5 | },
6 | "frameworks": {
7 | "uap10.0": {}
8 | },
9 | "runtimes": {
10 | "win10-arm": {},
11 | "win10-arm-aot": {},
12 | "win10-x86": {},
13 | "win10-x86-aot": {},
14 | "win10-x64": {},
15 | "win10-x64-aot": {}
16 | }
17 | }
--------------------------------------------------------------------------------
/Stuart/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.InteropServices;
3 |
4 | [assembly: AssemblyTitle("Stuart")]
5 | [assembly: AssemblyDescription("Shawn's Terrific Universal App for photogRaph Tweaking")]
6 | [assembly: AssemblyProduct("Stuart")]
7 | [assembly: AssemblyCopyright("Copyright © Shawn Hargreaves 2015")]
8 |
9 | [assembly: AssemblyVersion("1.0.0.0")]
10 | [assembly: AssemblyFileVersion("1.0.0.0")]
11 | [assembly: ComVisible(false)]
--------------------------------------------------------------------------------
/Stuart/App.xaml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Shawn Hargreaves
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Stuart/Properties/Default.rd.xml:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
20 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/Stuart/EffectPropertiesControl.xaml:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/Stuart/Converters.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Windows.UI.Xaml;
3 | using Windows.UI.Xaml.Data;
4 |
5 | namespace Stuart
6 | {
7 | class EnumToIntConverter : IValueConverter
8 | {
9 | public object Convert(object value, Type targetType, object parameter, string language)
10 | {
11 | return (int)value;
12 | }
13 |
14 | public object ConvertBack(object value, Type targetType, object parameter, string language)
15 | {
16 | return value;
17 | }
18 | }
19 |
20 |
21 | class NullToBooleanConverter : IValueConverter
22 | {
23 | public object Convert(object value, Type targetType, object parameter, string language)
24 | {
25 | return value != null;
26 | }
27 |
28 | public object ConvertBack(object value, Type targetType, object parameter, string language)
29 | {
30 | throw new NotImplementedException();
31 | }
32 | }
33 |
34 |
35 | class BooleanToVisibilityConverter : IValueConverter
36 | {
37 | public object Convert(object value, Type targetType, object parameter, string language)
38 | {
39 | return System.Convert.ToBoolean(value) ? Visibility.Visible : Visibility.Collapsed;
40 | }
41 |
42 | public object ConvertBack(object value, Type targetType, object parameter, string language)
43 | {
44 | throw new NotImplementedException();
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/Stuart/EditGroupControl.xaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq;
3 | using Windows.UI.Xaml;
4 | using Windows.UI.Xaml.Controls;
5 |
6 | namespace Stuart
7 | {
8 | // UI codebehind for configuring an EditGroup.
9 | public sealed partial class EditGroupControl : UserControl
10 | {
11 | public static EffectType[] EffectTypes
12 | {
13 | get { return Enum.GetValues(typeof(EffectType)).Cast().ToArray(); }
14 | }
15 |
16 |
17 | public EditGroupControl()
18 | {
19 | this.InitializeComponent();
20 | }
21 |
22 |
23 | void UndoRegionEdit_Click(object sender, RoutedEventArgs e)
24 | {
25 | var edit = (EditGroup)DataContext;
26 |
27 | edit.UndoRegionEdit();
28 | }
29 |
30 |
31 | void NewEffect_Click(object sender, RoutedEventArgs e)
32 | {
33 | var edit = (EditGroup)DataContext;
34 |
35 | var newEffect = new Effect(edit);
36 |
37 | edit.Effects.Add(newEffect);
38 | edit.Parent.SelectedEffect = newEffect;
39 | }
40 |
41 |
42 | void DeleteButton_Click(object sender, RoutedEventArgs e)
43 | {
44 | var effect = (Effect)effectList.SelectedValue;
45 |
46 | var edit = effect.Parent;
47 |
48 | effect.Dispose();
49 |
50 | if (edit.Effects.Count == 0)
51 | {
52 | edit.Dispose();
53 | }
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/Stuart/App.xaml.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using Windows.ApplicationModel.Activation;
3 | using Windows.Storage;
4 | using Windows.UI.Xaml;
5 | using Windows.UI.Xaml.Controls;
6 |
7 | namespace Stuart
8 | {
9 | // Provides application-specific behavior to supplement the default Application class.
10 | sealed partial class App : Application
11 | {
12 | public App()
13 | {
14 | this.InitializeComponent();
15 | }
16 |
17 |
18 | protected override void OnLaunched(LaunchActivatedEventArgs args)
19 | {
20 | Initialize(args.PreviousExecutionState);
21 | }
22 |
23 |
24 | protected override void OnFileActivated(FileActivatedEventArgs args)
25 | {
26 | Initialize(args.Files);
27 | }
28 |
29 |
30 | void Initialize(object launchArg)
31 | {
32 | Frame rootFrame = Window.Current.Content as Frame;
33 |
34 | if (rootFrame == null)
35 | {
36 | rootFrame = new Frame();
37 |
38 | Window.Current.Content = rootFrame;
39 | }
40 |
41 | if (rootFrame.Content == null)
42 | {
43 | rootFrame.Navigate(typeof(MainPage), launchArg);
44 | }
45 | else
46 | {
47 | ((MainPage)rootFrame.Content).TryLoadPhoto(launchArg as IReadOnlyList);
48 | }
49 |
50 | Window.Current.Activate();
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/Stuart/CachedImage.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Graphics.Canvas;
2 | using System.Linq;
3 |
4 | namespace Stuart
5 | {
6 | // Caches expensive image effect results in a rendertarget, to minimize repeated recomputation.
7 | class CachedImage
8 | {
9 | CanvasRenderTarget cachedImage;
10 |
11 | bool isCacheValid;
12 | object[] cacheKeys;
13 |
14 |
15 | public ICanvasImage Get(params object[] keys)
16 | {
17 | if (!isCacheValid)
18 | return null;
19 |
20 | if (keys != null && (cacheKeys == null || !keys.SequenceEqual(cacheKeys)))
21 | return null;
22 |
23 | return cachedImage;
24 | }
25 |
26 |
27 | public ICanvasImage Cache(Photo photo, ICanvasImage image, params object[] keys)
28 | {
29 | if (cachedImage == null)
30 | {
31 | cachedImage = new CanvasRenderTarget(photo.SourceBitmap.Device, photo.Size.X, photo.Size.Y, 96);
32 | }
33 |
34 | using (var drawingSession = cachedImage.CreateDrawingSession())
35 | {
36 | drawingSession.Blend = CanvasBlend.Copy;
37 | drawingSession.DrawImage(image);
38 | }
39 |
40 | isCacheValid = true;
41 | cacheKeys = keys;
42 |
43 | return cachedImage;
44 | }
45 |
46 |
47 | public void Reset()
48 | {
49 | isCacheValid = false;
50 | }
51 |
52 |
53 | public void RecoverAfterDeviceLost()
54 | {
55 | if (cachedImage != null)
56 | {
57 | cachedImage.Dispose();
58 | cachedImage = null;
59 | }
60 |
61 | isCacheValid = false;
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/Stuart.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 14
4 | VisualStudioVersion = 14.0.23107.0
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stuart", "Stuart\Stuart.csproj", "{A9368EF5-472F-4168-A20D-917D172B0FC1}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|ARM = Debug|ARM
11 | Debug|x64 = Debug|x64
12 | Debug|x86 = Debug|x86
13 | Release|ARM = Release|ARM
14 | Release|x64 = Release|x64
15 | Release|x86 = Release|x86
16 | EndGlobalSection
17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
18 | {A9368EF5-472F-4168-A20D-917D172B0FC1}.Debug|ARM.ActiveCfg = Debug|ARM
19 | {A9368EF5-472F-4168-A20D-917D172B0FC1}.Debug|ARM.Build.0 = Debug|ARM
20 | {A9368EF5-472F-4168-A20D-917D172B0FC1}.Debug|ARM.Deploy.0 = Debug|ARM
21 | {A9368EF5-472F-4168-A20D-917D172B0FC1}.Debug|x64.ActiveCfg = Debug|x64
22 | {A9368EF5-472F-4168-A20D-917D172B0FC1}.Debug|x64.Build.0 = Debug|x64
23 | {A9368EF5-472F-4168-A20D-917D172B0FC1}.Debug|x64.Deploy.0 = Debug|x64
24 | {A9368EF5-472F-4168-A20D-917D172B0FC1}.Debug|x86.ActiveCfg = Debug|x86
25 | {A9368EF5-472F-4168-A20D-917D172B0FC1}.Debug|x86.Build.0 = Debug|x86
26 | {A9368EF5-472F-4168-A20D-917D172B0FC1}.Debug|x86.Deploy.0 = Debug|x86
27 | {A9368EF5-472F-4168-A20D-917D172B0FC1}.Release|ARM.ActiveCfg = Release|ARM
28 | {A9368EF5-472F-4168-A20D-917D172B0FC1}.Release|ARM.Build.0 = Release|ARM
29 | {A9368EF5-472F-4168-A20D-917D172B0FC1}.Release|ARM.Deploy.0 = Release|ARM
30 | {A9368EF5-472F-4168-A20D-917D172B0FC1}.Release|x64.ActiveCfg = Release|x64
31 | {A9368EF5-472F-4168-A20D-917D172B0FC1}.Release|x64.Build.0 = Release|x64
32 | {A9368EF5-472F-4168-A20D-917D172B0FC1}.Release|x64.Deploy.0 = Release|x64
33 | {A9368EF5-472F-4168-A20D-917D172B0FC1}.Release|x86.ActiveCfg = Release|x86
34 | {A9368EF5-472F-4168-A20D-917D172B0FC1}.Release|x86.Build.0 = Release|x86
35 | {A9368EF5-472F-4168-A20D-917D172B0FC1}.Release|x86.Deploy.0 = Release|x86
36 | EndGlobalSection
37 | GlobalSection(SolutionProperties) = preSolution
38 | HideSolutionNode = FALSE
39 | EndGlobalSection
40 | EndGlobal
41 |
--------------------------------------------------------------------------------
/Stuart/Observable.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Collections.Specialized;
3 | using System.ComponentModel;
4 | using System.Runtime.CompilerServices;
5 |
6 | namespace Stuart
7 | {
8 | // Helper for implementing INotifyPropertyChanged.
9 | public abstract class Observable : INotifyPropertyChanged
10 | {
11 | public event PropertyChangedEventHandler PropertyChanged;
12 |
13 |
14 | protected void NotifyPropertyChanged(object sender, PropertyChangedEventArgs e)
15 | {
16 | if (PropertyChanged != null)
17 | {
18 | PropertyChanged(sender, e);
19 | }
20 | }
21 |
22 |
23 | protected void NotifyPropertyChanged(string propertyName)
24 | {
25 | NotifyPropertyChanged(this, new PropertyChangedEventArgs(propertyName));
26 | }
27 |
28 |
29 | protected void NotifyCollectionChanged(object sender, NotifyCollectionChangedEventArgs e, string propertyName)
30 | {
31 | // Unsubscribe property change events of items that were removed from the collection.
32 | if (e.OldItems != null)
33 | {
34 | foreach (INotifyPropertyChanged old in e.OldItems)
35 | {
36 | old.PropertyChanged -= NotifyPropertyChanged;
37 | }
38 | }
39 |
40 | // Subscribe to the property change events of newly added items.
41 | if (e.NewItems != null)
42 | {
43 | foreach (INotifyPropertyChanged item in e.NewItems)
44 | {
45 | item.PropertyChanged += NotifyPropertyChanged;
46 | }
47 | }
48 |
49 | // Also notify listeners that the collection itself has changed.
50 | NotifyPropertyChanged(propertyName);
51 | }
52 |
53 |
54 | protected void SetField(ref T field, T value, [CallerMemberName] string propertyName = null)
55 | {
56 | if (EqualityComparer.Default.Equals(field, value))
57 | return;
58 |
59 | field = value;
60 |
61 | NotifyPropertyChanged(propertyName);
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/Stuart/Stuart.appxmanifest:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Stuart Photo Editor
7 | Shawn Hargreaves
8 | Assets\StoreLogo.png
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | Stuart Photo
27 |
28 | .jpg
29 | .jpeg
30 | .png
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/Stuart/ExtensionMethods.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Numerics;
5 | using Windows.Foundation;
6 | using Windows.UI;
7 |
8 | namespace Stuart
9 | {
10 | static class BinaryWriterExtensions
11 | {
12 | public static void WriteCollection(this BinaryWriter writer, ICollection collection, Action writeItem)
13 | {
14 | writer.Write(collection.Count);
15 |
16 | foreach (var item in collection)
17 | {
18 | writeItem(item);
19 | }
20 | }
21 |
22 |
23 | public static void WriteByteArray(this BinaryWriter writer, byte[] array)
24 | {
25 | if (array == null)
26 | {
27 | writer.Write(0);
28 | }
29 | else
30 | {
31 | writer.Write(array.Length);
32 | writer.Write(array);
33 | }
34 | }
35 |
36 |
37 | public static void WriteColor(this BinaryWriter writer, Color color)
38 | {
39 | writer.Write(color.R);
40 | writer.Write(color.G);
41 | writer.Write(color.B);
42 | writer.Write(color.A);
43 | }
44 |
45 |
46 | public static void WriteRect(this BinaryWriter writer, Rect rect)
47 | {
48 | writer.Write(rect.X);
49 | writer.Write(rect.Y);
50 | writer.Write(rect.Width);
51 | writer.Write(rect.Height);
52 | }
53 |
54 |
55 | public static void WriteVector2(this BinaryWriter writer, Vector2 vector)
56 | {
57 | writer.Write(vector.X);
58 | writer.Write(vector.Y);
59 | }
60 | }
61 |
62 |
63 | static class BinaryReaderExtensions
64 | {
65 | public static void ReadCollection(this BinaryReader reader, ICollection collection, Func readItem)
66 | {
67 | collection.Clear();
68 |
69 | var count = reader.ReadInt32();
70 |
71 | for (int i = 0; i < count; i++)
72 | {
73 | collection.Add(readItem());
74 | }
75 | }
76 |
77 |
78 | public static byte[] ReadByteArray(this BinaryReader reader)
79 | {
80 | var count = reader.ReadInt32();
81 |
82 | return (count == 0) ? null : reader.ReadBytes(count);
83 | }
84 |
85 |
86 | public static Color ReadColor(this BinaryReader reader)
87 | {
88 | Color color;
89 |
90 | color.R = reader.ReadByte();
91 | color.G = reader.ReadByte();
92 | color.B = reader.ReadByte();
93 | color.A = reader.ReadByte();
94 |
95 | return color;
96 | }
97 |
98 |
99 | public static Rect ReadRect(this BinaryReader reader)
100 | {
101 | Rect rect;
102 |
103 | rect.X = reader.ReadDouble();
104 | rect.Y = reader.ReadDouble();
105 | rect.Width = reader.ReadDouble();
106 | rect.Height = reader.ReadDouble();
107 |
108 | return rect;
109 | }
110 |
111 |
112 | public static Vector2 ReadVector2(this BinaryReader reader)
113 | {
114 | Vector2 vector;
115 |
116 | vector.X = reader.ReadSingle();
117 | vector.Y = reader.ReadSingle();
118 |
119 | return vector;
120 | }
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Shawn's Terrific Universal App for photogRaph Tweaking
2 |
3 | This is a simple, powerful, and contrivedly acronymed Windows 10 photo editing app,
4 | created during an app building exercise to test the [Win2D](http:/github.com/microsoft/win2d) graphics API.
5 |
6 |
7 | #### Where to get it
8 |
9 | - [Download from the Store](https://www.microsoft.com/store/apps/9NBLGGH1XSQK)
10 | - Or clone the source from github and load Stuart.sln into Visual Studio 2015
11 |
12 |
13 | #### Features
14 |
15 | - Use a rich set of image effects to tweak your photos
16 | - Apply effects to the whole image or just selected parts of it
17 | - Feather selected regions for smooth transitions
18 | - Runs on Windows 10 PCs and phones
19 |
20 |
21 | #### Supported effects
22 |
23 | - Framing:
24 | - Crop
25 | - Straighten
26 | - Color adjustment:
27 | - Exposure
28 | - Highlights & Shadows
29 | - Temperature & Tint
30 | - Contrast
31 | - Saturate
32 | - Stylize:
33 | - Grayscale
34 | - Sepia
35 | - Vignette
36 | - Special effects:
37 | - Blur
38 | - Motion blur
39 | - Sharpen
40 | - Edge detection
41 | - Emboss
42 | - Invert
43 | - Posterize
44 |
45 |
46 | #### How to use it
47 |
48 | Start by loading a photo:
49 |
50 | 
51 |
52 | At the top of the UI are the open, save, save-as and help buttons. Below this is a list of edit groups,
53 | each of which contains a list of effects. To start off, there is just one edit group, containing a single
54 | Crop effect.
55 |
56 | To view different parts of the image, use pinch zoom or the A and Z keys.
57 |
58 | Click the '+' buttons to add new effects or edit groups. Drag things to reorder them, and use the 'x'
59 | button to delete the selected effect. The pair-of-eyes icons are used to show or hide effects and edit
60 | groups - this is handy for doing quick before/after comparisons.
61 |
62 | To adjust the settings of an effect, click the '...' to the right of the effect name, which brings
63 | up a set of sliders. Here is our photo after Highlights, Temperature, and Saturate adjustment:
64 |
65 | 
66 |
67 | For a retro look, try Sepia and Vignette, with some Contrast thrown in for good measure:
68 |
69 | 
70 |
71 | Sometimes you want to adjust only part of a photo without affecting other areas. To do this, click
72 | the square icon at the top of an edit group. This brings up region editing options for that group.
73 | All the effects inside the group will apply only to the selected region.
74 |
75 | 
76 |
77 | The region editing options are:
78 | - Selection mode (rectangle, ellipse, freehand, or magic wand)
79 | - Selection operation (replace region, add to region, subtract from region, or invert region)
80 | - Undo region edit
81 | - Show region (another pair-of-eyes icon - if enabled, the region border is displayed in pink while everything outside the region is grayed out)
82 | - Feather - softens the edge of the region, so its effects will smoothly crossfade with the outside unaffected part of the photo
83 | - Dilate - expands or shrinks the selected region (useful for cleaning up magic wand selections)
84 |
85 | Magic wand mode selects areas of the photo that have similar colors. To use it,
86 | click and drag on the photo. The initial click point defines what color to select,
87 | and how far you drag controls the color matching tolerance.
88 |
89 | While in region editing mode, clicking on the photo will change the selected region
90 | rather than panning or zooming the view. If you want to pan or zoom again, click the
91 | square 'edit region' button a second time to turn off region edit mode.
92 |
93 | Here is our photo using a region to apply Exposure and Saturation effects only to
94 | the foreground trees and rocks, brightening them up without changing the sky. It uses
95 | a second edit group to blur the distant hills only in the bottom left of the photo:
96 |
97 | 
98 |
99 | And finally, something rather more extreme: this screenshot uses two edit groups to apply
100 | different effects to the sky vs. ground:
101 |
102 | 
103 |
--------------------------------------------------------------------------------
/Stuart/Photo.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Graphics.Canvas;
2 | using System;
3 | using System.Collections.ObjectModel;
4 | using System.IO;
5 | using System.Numerics;
6 | using System.Threading.Tasks;
7 | using Windows.Foundation;
8 | using Windows.Graphics.DirectX;
9 | using Windows.Storage;
10 |
11 | namespace Stuart
12 | {
13 | // Top level DOM type stores the photo plus a list of edits.
14 | public class Photo : Observable
15 | {
16 | public CanvasBitmap SourceBitmap
17 | {
18 | get { return sourceBitmap; }
19 | private set { SetField(ref sourceBitmap, value); }
20 | }
21 |
22 | CanvasBitmap sourceBitmap;
23 |
24 | DirectXPixelFormat bitmapFormat;
25 | byte[] bitmapData;
26 |
27 |
28 | public Vector2 Size { get; private set; }
29 |
30 |
31 | public ObservableCollection Edits { get; } = new ObservableCollection();
32 |
33 |
34 | public Effect SelectedEffect
35 | {
36 | get { return selectedEffect; }
37 | set { SetField(ref selectedEffect, value); }
38 | }
39 |
40 | Effect selectedEffect;
41 |
42 |
43 | public Photo()
44 | {
45 | Edits.CollectionChanged += (sender, e) => NotifyCollectionChanged(sender, e, "Edits");
46 | }
47 |
48 |
49 | public async Task Load(CanvasDevice device, StorageFile file)
50 | {
51 | using (var stream = await file.OpenReadAsync())
52 | {
53 | SourceBitmap = await CanvasBitmap.LoadAsync(device, stream);
54 | }
55 |
56 | bitmapFormat = sourceBitmap.Format;
57 | bitmapData = sourceBitmap.GetPixelBytes();
58 |
59 | Size = sourceBitmap.Size.ToVector2();
60 |
61 | Edits.Clear();
62 | Edits.Add(new EditGroup(this));
63 |
64 | SelectedEffect = null;
65 | }
66 |
67 |
68 | public async Task Save(StorageFile file)
69 | {
70 | var image = GetImage();
71 |
72 | // Measure the extent of the image (which may be cropped).
73 | Rect imageBounds;
74 |
75 | using (var commandList = new CanvasCommandList(sourceBitmap.Device))
76 | using (var drawingSession = commandList.CreateDrawingSession())
77 | {
78 | imageBounds = image.GetBounds(drawingSession);
79 | }
80 |
81 | // Rasterize the image into a rendertarget.
82 | using (var renderTarget = new CanvasRenderTarget(sourceBitmap.Device, (float)imageBounds.Width, (float)imageBounds.Height, 96))
83 | {
84 | using (var drawingSession = renderTarget.CreateDrawingSession())
85 | {
86 | drawingSession.Blend = CanvasBlend.Copy;
87 |
88 | drawingSession.DrawImage(image, -(float)imageBounds.X, -(float)imageBounds.Y);
89 | }
90 |
91 | // Save it out.
92 | var format = file.FileType.Equals(".png", StringComparison.OrdinalIgnoreCase) ? CanvasBitmapFileFormat.Png : CanvasBitmapFileFormat.Jpeg;
93 |
94 | using (var stream = await file.OpenAsync(FileAccessMode.ReadWrite))
95 | {
96 | stream.Size = 0;
97 |
98 | await renderTarget.SaveAsync(stream, format);
99 | }
100 | }
101 | }
102 |
103 |
104 | public void RecoverAfterDeviceLost(CanvasDevice device)
105 | {
106 | SourceBitmap = CanvasBitmap.CreateFromBytes(device, bitmapData, (int)Size.X, (int)Size.Y, bitmapFormat);
107 |
108 | foreach (var edit in Edits)
109 | {
110 | edit.RecoverAfterDeviceLost();
111 | }
112 | }
113 |
114 |
115 | public void SaveSuspendedState(BinaryWriter writer)
116 | {
117 | writer.Write((int)bitmapFormat);
118 | writer.WriteByteArray(bitmapData);
119 |
120 | writer.WriteVector2(Size);
121 |
122 | writer.WriteCollection(Edits, edit => edit.SaveSuspendedState(writer));
123 | }
124 |
125 |
126 | public void RestoreSuspendedState(CanvasDevice device, BinaryReader reader)
127 | {
128 | bitmapFormat = (DirectXPixelFormat)reader.ReadInt32();
129 | bitmapData = reader.ReadByteArray();
130 |
131 | Size = reader.ReadVector2();
132 |
133 | reader.ReadCollection(Edits, () => EditGroup.RestoreSuspendedState(this, reader));
134 |
135 | RecoverAfterDeviceLost(device);
136 | }
137 |
138 |
139 | public ICanvasImage GetImage()
140 | {
141 | ICanvasImage image = sourceBitmap;
142 |
143 | foreach (var edit in Edits)
144 | {
145 | image = edit.Apply(image);
146 | }
147 |
148 | return image;
149 | }
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/Stuart/MainPage.xaml:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
31 |
32 |
37 |
38 |
43 |
44 |
45 |
49 |
50 |
54 |
55 |
56 |
57 |
58 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
81 |
82 |
83 |
84 |
85 |
89 |
90 |
91 |
92 |
93 |
94 |
100 |
101 |
108 |
109 |
110 |
111 |
114 |
115 |
116 |
--------------------------------------------------------------------------------
/Stuart/Effect.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Graphics.Canvas;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.IO;
5 | using System.Reflection;
6 | using Windows.Foundation;
7 | using Windows.UI;
8 | using Windows.UI.Xaml;
9 |
10 | namespace Stuart
11 | {
12 | // DOM type representing a single image processing effect.
13 | public class Effect : Observable, IDisposable
14 | {
15 | public EditGroup Parent { get; private set; }
16 |
17 | readonly Dictionary parameters = new Dictionary();
18 |
19 |
20 | public EffectType Type
21 | {
22 | get { return type; }
23 | set { SetField(ref type, value); }
24 | }
25 |
26 | EffectType type;
27 |
28 |
29 | public bool IsEnabled
30 | {
31 | get { return isEnabled; }
32 | set { SetField(ref isEnabled, value); }
33 | }
34 |
35 | bool isEnabled = true;
36 |
37 |
38 | public Effect(EditGroup parent)
39 | {
40 | Parent = parent;
41 | }
42 |
43 |
44 | public void Dispose()
45 | {
46 | Parent.Effects.Remove(this);
47 | }
48 |
49 |
50 | public object GetParameter(EffectParameter parameter)
51 | {
52 | var parameterName = ParameterName(parameter);
53 |
54 | object result;
55 |
56 | return parameters.TryGetValue(parameterName, out result) ? result : parameter.Default;
57 | }
58 |
59 |
60 | public void SetParameter(EffectParameter parameter, object value)
61 | {
62 | var parameterName = ParameterName(parameter);
63 |
64 | parameters[parameterName] = value;
65 |
66 | NotifyPropertyChanged(parameterName);
67 | }
68 |
69 |
70 | string ParameterName(EffectParameter parameter)
71 | {
72 | return Type.ToString() + '.' + parameter.Name;
73 | }
74 |
75 |
76 | public ICanvasImage Apply(ICanvasImage image, ref Rect? bounds)
77 | {
78 | if (!IsEnabled)
79 | return image;
80 |
81 | var metadata = EffectMetadata.Get(type);
82 |
83 | // Instantiate the effect.
84 | var effect = (ICanvasImage)Activator.CreateInstance(metadata.ImplementationType);
85 |
86 | // Set the effect input.
87 | SetProperty(effect, "Source", image);
88 |
89 | // Set configurable parameter values.
90 | foreach (var parameter in metadata.Parameters)
91 | {
92 | var value = GetParameter(parameter);
93 |
94 | SetProperty(effect, parameter.Name, value);
95 |
96 | // Track the image bounds if cropping changes them.
97 | if (this.Type == EffectType.Crop && parameter.Name == "SourceRectangle")
98 | {
99 | bounds = bounds.HasValue ? RectHelper.Intersect(bounds.Value, (Rect)value) : (Rect)value;
100 | }
101 | }
102 |
103 | // Set any constant values.
104 | foreach (var constant in metadata.Constants)
105 | {
106 | SetProperty(effect, constant.Key, constant.Value);
107 | }
108 |
109 | return effect;
110 | }
111 |
112 |
113 | static void SetProperty(object instance, string propertyName, object value)
114 | {
115 | instance.GetType()
116 | .GetRuntimeProperty(propertyName)
117 | .SetValue(instance, value);
118 | }
119 |
120 |
121 | public void SaveSuspendedState(BinaryWriter writer)
122 | {
123 | writer.Write(IsEnabled);
124 | writer.Write((int)Type);
125 |
126 | writer.WriteCollection(parameters, parameter =>
127 | {
128 | writer.Write(parameter.Key);
129 | writer.Write(parameter.Value.GetType().Name);
130 |
131 | if (parameter.Value is Color)
132 | {
133 | writer.WriteColor((Color)parameter.Value);
134 | }
135 | else if (parameter.Value is Rect)
136 | {
137 | writer.WriteRect((Rect)parameter.Value);
138 | }
139 | else
140 | {
141 | writer.Write(parameter.Value as dynamic);
142 | }
143 | });
144 | }
145 |
146 |
147 | public static Effect RestoreSuspendedState(EditGroup parent, BinaryReader reader)
148 | {
149 | var effect = new Effect(parent);
150 |
151 | effect.IsEnabled = reader.ReadBoolean();
152 | effect.Type = (EffectType)reader.ReadInt32();
153 |
154 | reader.ReadCollection(effect.parameters, () =>
155 | {
156 | string key = reader.ReadString();
157 | object value;
158 |
159 | switch (reader.ReadString())
160 | {
161 | case "Single":
162 | value = reader.ReadSingle();
163 | break;
164 |
165 | case "Int32":
166 | value = reader.ReadInt32();
167 | break;
168 |
169 | case "Boolean":
170 | value = reader.ReadBoolean();
171 | break;
172 |
173 | case "Color":
174 | value = reader.ReadColor();
175 | break;
176 |
177 | case "Rect":
178 | value = reader.ReadRect();
179 | break;
180 |
181 | default:
182 | throw new NotImplementedException();
183 | }
184 |
185 | return new KeyValuePair(key, value);
186 | });
187 |
188 | return effect;
189 | }
190 | }
191 | }
192 |
--------------------------------------------------------------------------------
/Stuart/EditGroupControl.xaml:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
22 |
23 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
49 |
50 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
78 |
79 |
80 |
81 |
82 |
83 |
89 |
90 |
91 |
92 |
93 |
96 |
97 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
109 |
110 |
114 |
115 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
--------------------------------------------------------------------------------
/Stuart/Stuart.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Debug
6 | x86
7 | {A9368EF5-472F-4168-A20D-917D172B0FC1}
8 | AppContainerExe
9 | Properties
10 | Stuart
11 | Stuart
12 | en-US
13 | UAP
14 | 10.0.10240.0
15 | 10.0.10240.0
16 | 14
17 | true
18 | 512
19 | {A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
20 | Stuart_StoreKey.pfx
21 | False
22 | x86|x64|arm
23 | CEA259A385754D57A5065ACB5F646071C2BE3E50
24 | Always
25 |
26 |
27 | true
28 | bin\ARM\Debug\
29 | DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP
30 | ;2008
31 | full
32 | ARM
33 | false
34 | prompt
35 | true
36 |
37 |
38 | bin\ARM\Release\
39 | TRACE;NETFX_CORE;WINDOWS_UWP
40 | true
41 | ;2008
42 | pdbonly
43 | ARM
44 | false
45 | prompt
46 | true
47 | true
48 |
49 |
50 | true
51 | bin\x64\Debug\
52 | DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP
53 | ;2008
54 | full
55 | x64
56 | false
57 | prompt
58 | true
59 |
60 |
61 | bin\x64\Release\
62 | TRACE;NETFX_CORE;WINDOWS_UWP
63 | true
64 | ;2008
65 | pdbonly
66 | x64
67 | false
68 | prompt
69 | true
70 | true
71 |
72 |
73 | true
74 | bin\x86\Debug\
75 | DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP
76 | ;2008
77 | full
78 | x86
79 | false
80 | prompt
81 | true
82 |
83 |
84 | bin\x86\Release\
85 | TRACE;NETFX_CORE;WINDOWS_UWP
86 | true
87 | ;2008
88 | pdbonly
89 | x86
90 | false
91 | prompt
92 | true
93 | true
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 | App.xaml
103 |
104 |
105 |
106 |
107 |
108 | EffectPropertiesControl.xaml
109 |
110 |
111 |
112 |
113 |
114 | MainPage.xaml
115 |
116 |
117 |
118 | EditGroupControl.xaml
119 |
120 |
121 |
122 |
123 |
124 |
125 | Designer
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 | MSBuild:Compile
143 | Designer
144 |
145 |
146 | Designer
147 | MSBuild:Compile
148 |
149 |
150 | MSBuild:Compile
151 | Designer
152 |
153 |
154 | Designer
155 | MSBuild:Compile
156 |
157 |
158 |
159 |
160 | Windows Mobile Extensions for the UWP
161 |
162 |
163 |
164 | 14.0
165 |
166 |
167 |
174 |
--------------------------------------------------------------------------------
/Stuart/EffectPropertiesControl.xaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.ComponentModel;
4 | using System.Linq;
5 | using System.Numerics;
6 | using System.Reflection;
7 | using System.Text.RegularExpressions;
8 | using Windows.Foundation;
9 | using Windows.UI;
10 | using Windows.UI.Xaml;
11 | using Windows.UI.Xaml.Controls;
12 |
13 | namespace Stuart
14 | {
15 | public sealed partial class EffectPropertiesControl : UserControl
16 | {
17 | public Effect CurrentEffect
18 | {
19 | get { return (Effect)GetValue(CurrentEffectProperty); }
20 | set { SetValue(CurrentEffectProperty, value); }
21 | }
22 |
23 | public static readonly DependencyProperty CurrentEffectProperty =
24 | DependencyProperty.Register(
25 | "CurrentEffect",
26 | typeof(Effect),
27 | typeof(EffectPropertiesControl),
28 | new PropertyMetadata(null, CurrentEffectChanged));
29 |
30 |
31 | public EffectPropertiesControl()
32 | {
33 | this.InitializeComponent();
34 | }
35 |
36 |
37 | static void CurrentEffectChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
38 | {
39 | var self = (EffectPropertiesControl)d;
40 |
41 | // Unsubscribe events from the previous effect.
42 | if (e.OldValue != null)
43 | {
44 | ((Effect)e.OldValue).PropertyChanged -= self.Effect_PropertyChanged;
45 | }
46 |
47 | // Listen for property changes on the newly selected effect.
48 | if (e.NewValue != null)
49 | {
50 | ((Effect)e.NewValue).PropertyChanged += self.Effect_PropertyChanged;
51 | }
52 |
53 | self.CreateWidgets();
54 | }
55 |
56 |
57 | void Effect_PropertyChanged(object sender, PropertyChangedEventArgs e)
58 | {
59 | if (e.PropertyName == "Type")
60 | {
61 | CreateWidgets();
62 | }
63 | }
64 |
65 |
66 | void CreateWidgets()
67 | {
68 | grid.Children.Clear();
69 |
70 | var effect = CurrentEffect;
71 |
72 | if (effect == null)
73 | return;
74 |
75 | var metadata = EffectMetadata.Get(effect.Type);
76 |
77 | // Create XAML elements and choose their label strings.
78 | var widgets = new List();
79 | var widgetNames = new List();
80 |
81 | foreach (var parameter in metadata.Parameters)
82 | {
83 | CreateParameterWidgets(effect, parameter, widgets, widgetNames);
84 |
85 | if (widgetNames.Count < widgets.Count)
86 | {
87 | widgetNames.Add(FormatParameterName(parameter.Name));
88 | }
89 | }
90 |
91 | // Populate the grid.
92 | for (int row = 0; row < widgets.Count; row++)
93 | {
94 | var label = new TextBlock { Text = widgetNames[row] };
95 |
96 | AddToGrid(label, row, 0);
97 | AddToGrid(widgets[row], row, 2);
98 | }
99 | }
100 |
101 |
102 | static void CreateParameterWidgets(Effect effect, EffectParameter parameter, List widgets, List widgetNames)
103 | {
104 | if (parameter.Default is float)
105 | {
106 | widgets.Add(CreateFloatWidget(effect, parameter));
107 | }
108 | else if (parameter.Default is int)
109 | {
110 | widgets.Add(CreateIntWidget(effect, parameter));
111 | }
112 | else if (parameter.Default is bool)
113 | {
114 | widgets.Add(CreateBoolWidget(effect, parameter));
115 | }
116 | else if (parameter.Default is Color)
117 | {
118 | widgets.Add(CreateColorWidget(effect, parameter));
119 | }
120 | else if (parameter.Default is Rect)
121 | {
122 | CreateRectWidgets(effect, parameter, widgets, widgetNames);
123 | }
124 | else
125 | {
126 | throw new NotImplementedException();
127 | }
128 | }
129 |
130 |
131 | static UIElement CreateFloatWidget(Effect effect, EffectParameter parameter)
132 | {
133 | float valueScale = (parameter.Max - parameter.Min) / 100;
134 |
135 | var slider = new Slider()
136 | {
137 | Value = ((float)effect.GetParameter(parameter) - parameter.Min) / valueScale
138 | };
139 |
140 | slider.ValueChanged += (sender, e) =>
141 | {
142 | effect.SetParameter(parameter, parameter.Min + (float)e.NewValue * valueScale);
143 | };
144 |
145 | return slider;
146 | }
147 |
148 |
149 | static UIElement CreateIntWidget(Effect effect, EffectParameter parameter)
150 | {
151 | var slider = new Slider()
152 | {
153 | Minimum = parameter.Min,
154 | Maximum = parameter.Max,
155 |
156 | Value = (int)effect.GetParameter(parameter)
157 | };
158 |
159 | slider.ValueChanged += (sender, e) =>
160 | {
161 | effect.SetParameter(parameter, (int)e.NewValue);
162 | };
163 |
164 | return slider;
165 | }
166 |
167 |
168 | static UIElement CreateBoolWidget(Effect effect, EffectParameter parameter)
169 | {
170 | var checkbox = new CheckBox()
171 | {
172 | IsChecked = (bool)effect.GetParameter(parameter)
173 | };
174 |
175 | checkbox.Checked += (sender, e) => { effect.SetParameter(parameter, true); };
176 | checkbox.Unchecked += (sender, e) => { effect.SetParameter(parameter, false); };
177 |
178 | return checkbox;
179 | }
180 |
181 |
182 | static UIElement CreateColorWidget(Effect effect, EffectParameter parameter)
183 | {
184 | var colorProperties = typeof(Colors).GetRuntimeProperties();
185 | var colorNames = colorProperties.Select(p => p.Name).ToList();
186 | var colorValues = colorProperties.Select(p => p.GetValue(null)).ToList();
187 |
188 | var combo = new ComboBox()
189 | {
190 | ItemsSource = colorNames,
191 | SelectedIndex = colorValues.IndexOf(effect.GetParameter(parameter))
192 | };
193 |
194 | combo.SelectionChanged += (sender, e) =>
195 | {
196 | effect.SetParameter(parameter, colorValues[combo.SelectedIndex]);
197 | };
198 |
199 | return combo;
200 | }
201 |
202 |
203 | static void CreateRectWidgets(Effect effect, EffectParameter parameter, List widgets, List widgetNames)
204 | {
205 | Photo photo = effect.Parent.Parent;
206 |
207 | // Read the current rectangle (infinity means not initialized).
208 | var initialValue = (Rect)effect.GetParameter(parameter);
209 |
210 | if (double.IsInfinity(initialValue.Width))
211 | {
212 | initialValue = photo.SourceBitmap.Bounds;
213 | }
214 |
215 | var topLeft = new Vector2((float)initialValue.Left, (float)initialValue.Top);
216 | var bottomRight = new Vector2((float)initialValue.Right, (float)initialValue.Bottom);
217 |
218 | // Create four sliders.
219 | for (int i = 0; i < 4; i++)
220 | {
221 | int whichSlider = i;
222 |
223 | var slider = new Slider();
224 |
225 | // Initialize the slider position.
226 | switch (whichSlider)
227 | {
228 | case 0:
229 | slider.Value = topLeft.X * 100 / photo.Size.X;
230 | break;
231 |
232 | case 1:
233 | slider.Value = bottomRight.X * 100 / photo.Size.X;
234 | break;
235 |
236 | case 2:
237 | slider.Value = topLeft.Y * 100 / photo.Size.Y;
238 | break;
239 |
240 | case 3:
241 | slider.Value = bottomRight.Y * 100 / photo.Size.Y;
242 | break;
243 | }
244 |
245 | // Respond to slider changes.
246 | slider.ValueChanged += (sender, e) =>
247 | {
248 | switch (whichSlider)
249 | {
250 | case 0:
251 | topLeft.X = (float)e.NewValue * photo.Size.X / 100;
252 | break;
253 |
254 | case 1:
255 | bottomRight.X = (float)e.NewValue * photo.Size.X / 100;
256 | break;
257 |
258 | case 2:
259 | topLeft.Y = (float)e.NewValue * photo.Size.Y / 100;
260 | break;
261 |
262 | case 3:
263 | bottomRight.Y = (float)e.NewValue * photo.Size.Y / 100;
264 | break;
265 | }
266 |
267 | // Make sure the rectangle never goes zero or negative.
268 | var tl = Vector2.Min(topLeft, photo.Size - Vector2.One);
269 | var br = Vector2.Max(bottomRight, tl + Vector2.One);
270 |
271 | effect.SetParameter(parameter, new Rect(tl.ToPoint(), br.ToPoint()));
272 | };
273 |
274 | widgets.Add(slider);
275 | }
276 |
277 | widgetNames.AddRange(new string[] { "Left", "Right", "Top", "Bottom" });
278 | }
279 |
280 |
281 | void AddToGrid(UIElement element, int row, int column)
282 | {
283 | element.SetValue(Grid.RowProperty, row);
284 | element.SetValue(Grid.ColumnProperty, column);
285 |
286 | grid.Children.Add(element);
287 | }
288 |
289 |
290 | static string FormatParameterName(string name)
291 | {
292 | return new Regex("(? Parameters = new List();
47 | public readonly Dictionary Constants = new Dictionary();
48 |
49 |
50 | public static EffectMetadata Get(EffectType effectType)
51 | {
52 | return metadata[effectType];
53 | }
54 |
55 |
56 | readonly static Dictionary metadata = new Dictionary
57 | {
58 | // Crop metadata.
59 | {
60 | EffectType.Crop, new EffectMetadata
61 | {
62 | ImplementationType = typeof(CropEffect),
63 |
64 | Parameters =
65 | {
66 | new EffectParameter { Name = "SourceRectangle", Default = new Rect(float.NegativeInfinity,
67 | float.NegativeInfinity,
68 | float.PositiveInfinity,
69 | float.PositiveInfinity) }
70 | }
71 | }
72 | },
73 |
74 | // Straighten metadata.
75 | {
76 | EffectType.Straighten, new EffectMetadata
77 | {
78 | ImplementationType = typeof(StraightenEffect),
79 |
80 | Parameters =
81 | {
82 | new EffectParameter { Name = "Angle", Default = 0f, Min = -(float)Math.PI / 16, Max = (float)Math.PI / 16 }
83 | },
84 |
85 | Constants =
86 | {
87 | { "MaintainSize", true }
88 | }
89 | }
90 | },
91 |
92 | // Exposure metadata.
93 | {
94 | EffectType.Exposure, new EffectMetadata
95 | {
96 | ImplementationType = typeof(ExposureEffect),
97 |
98 | Parameters =
99 | {
100 | new EffectParameter { Name = "Exposure", Default = 0f, Min = -2, Max = 2 }
101 | }
102 | }
103 | },
104 |
105 | // Highlights metadata.
106 | {
107 | EffectType.Highlights, new EffectMetadata
108 | {
109 | ImplementationType = typeof(HighlightsAndShadowsEffect),
110 |
111 | Parameters =
112 | {
113 | new EffectParameter { Name = "Highlights", Default = 0f, Min = -1, Max = 1 },
114 | new EffectParameter { Name = "Shadows", Default = 0f, Min = -1, Max = 1 },
115 | new EffectParameter { Name = "Clarity", Default = 0f, Min = -1, Max = 1 },
116 | new EffectParameter { Name = "MaskBlurAmount", Default = 0.25f, Min = 0, Max = 10 },
117 | }
118 | }
119 | },
120 |
121 | // Temperature metadata.
122 | {
123 | EffectType.Temp, new EffectMetadata
124 | {
125 | ImplementationType = typeof(TemperatureAndTintEffect),
126 |
127 | Parameters =
128 | {
129 | new EffectParameter { Name = "Temperature", Default = 0f, Min = -1, Max = 1 },
130 | new EffectParameter { Name = "Tint", Default = 0f, Min = -1, Max = 1 },
131 | }
132 | }
133 | },
134 |
135 | // Contrast metadata.
136 | {
137 | EffectType.Contrast, new EffectMetadata
138 | {
139 | ImplementationType = typeof(ContrastEffect),
140 |
141 | Parameters =
142 | {
143 | new EffectParameter { Name = "Contrast", Default = 0f, Min = -1, Max = 1 }
144 | }
145 | }
146 | },
147 |
148 | // Saturate metadata.
149 | {
150 | EffectType.Saturate, new EffectMetadata
151 | {
152 | ImplementationType = typeof(SaturationEffect),
153 |
154 | Parameters =
155 | {
156 | new EffectParameter { Name = "Saturation", Default = 0.5f, Min = 0, Max = 2 }
157 | }
158 | }
159 | },
160 |
161 | // Grayscale metadata.
162 | {
163 | EffectType.Gray, new EffectMetadata
164 | {
165 | ImplementationType = typeof(GrayscaleEffect)
166 | }
167 | },
168 |
169 | // Sepia metadata.
170 | {
171 | EffectType.Sepia, new EffectMetadata
172 | {
173 | ImplementationType = typeof(SepiaEffect),
174 |
175 | Parameters =
176 | {
177 | new EffectParameter { Name = "Intensity", Default = 0.5f, Min = 0, Max = 1 }
178 | }
179 | }
180 | },
181 |
182 | // Vignette metadata.
183 | {
184 | EffectType.Vignette, new EffectMetadata
185 | {
186 | ImplementationType = typeof(VignetteEffect),
187 |
188 | Parameters =
189 | {
190 | new EffectParameter { Name = "Amount", Default = 0.1f, Min = 0, Max = 1 },
191 | new EffectParameter { Name = "Curve", Default = 0.5f, Min = 0, Max = 1 },
192 | new EffectParameter { Name = "Color", Default = Colors.Black },
193 | }
194 | }
195 | },
196 |
197 | // Blur metadata.
198 | {
199 | EffectType.Blur, new EffectMetadata
200 | {
201 | ImplementationType = typeof(GaussianBlurEffect),
202 |
203 | Parameters =
204 | {
205 | new EffectParameter { Name = "BlurAmount", Default = 8f, Min = 0, Max = 100 }
206 | },
207 |
208 | Constants =
209 | {
210 | { "BorderMode", EffectBorderMode.Hard }
211 | }
212 | }
213 | },
214 |
215 | // Motion metadata.
216 | {
217 | EffectType.Motion, new EffectMetadata
218 | {
219 | ImplementationType = typeof(DirectionalBlurEffect),
220 |
221 | Parameters =
222 | {
223 | new EffectParameter { Name = "BlurAmount", Default = 8f, Min = 0, Max = 100 },
224 | new EffectParameter { Name = "Angle", Default = 0f, Min = 0, Max = (float)Math.PI },
225 | },
226 |
227 | Constants =
228 | {
229 | { "BorderMode", EffectBorderMode.Hard }
230 | }
231 | }
232 | },
233 |
234 | // Sharpen metadata.
235 | {
236 | EffectType.Sharpen, new EffectMetadata
237 | {
238 | ImplementationType = typeof(SharpenEffect),
239 |
240 | Parameters =
241 | {
242 | new EffectParameter { Name = "Amount", Default = 0f, Min = 0, Max = 10 },
243 | new EffectParameter { Name = "Threshold", Default = 0f, Min = 0, Max = 1 },
244 | }
245 | }
246 | },
247 |
248 | // Edge detection metadata.
249 | {
250 | EffectType.Edges, new EffectMetadata
251 | {
252 | ImplementationType = typeof(EdgeDetectionEffect),
253 |
254 | Parameters =
255 | {
256 | new EffectParameter { Name = "Amount", Default = 0.5f, Min = 0.01f, Max = 1 },
257 | new EffectParameter { Name = "BlurAmount", Default = 0f, Min = 0, Max = 2 },
258 | new EffectParameter { Name = "OverlayEdges", Default = false },
259 | }
260 | }
261 | },
262 |
263 | // Emboss metadata.
264 | {
265 | EffectType.Emboss, new EffectMetadata
266 | {
267 | ImplementationType = typeof(EmbossEffect),
268 |
269 | Parameters =
270 | {
271 | new EffectParameter { Name = "Amount", Default = 1f, Min = 0, Max = 10 },
272 | new EffectParameter { Name = "Angle", Default = 0f, Min = 0, Max = (float)Math.PI * 2 },
273 | }
274 | }
275 | },
276 |
277 | // Invert metadata.
278 | {
279 | EffectType.Invert, new EffectMetadata
280 | {
281 | ImplementationType = typeof(InvertEffect)
282 | }
283 | },
284 |
285 | // Posterize metadata.
286 | {
287 | EffectType.Posterize, new EffectMetadata
288 | {
289 | ImplementationType = typeof(PosterizeEffect),
290 |
291 | Parameters =
292 | {
293 | new EffectParameter { Name = "RedValueCount", Default = 4, Min = 2, Max = 16 },
294 | new EffectParameter { Name = "GreenValueCount", Default = 4, Min = 2, Max = 16 },
295 | new EffectParameter { Name = "BlueValueCount", Default = 4, Min = 2, Max = 16 },
296 | }
297 | }
298 | },
299 | };
300 | }
301 | }
302 |
--------------------------------------------------------------------------------
/Stuart/EditGroup.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Graphics.Canvas;
2 | using Microsoft.Graphics.Canvas.Effects;
3 | using Microsoft.Graphics.Canvas.Geometry;
4 | using System;
5 | using System.Collections.Generic;
6 | using System.Collections.ObjectModel;
7 | using System.IO;
8 | using System.Linq;
9 | using System.Numerics;
10 | using Windows.Foundation;
11 | using Windows.UI;
12 |
13 | namespace Stuart
14 | {
15 | public enum SelectionMode
16 | {
17 | Rectangle,
18 | Ellipse,
19 | Freehand,
20 | MagicWand
21 | }
22 |
23 |
24 | public enum SelectionOperation
25 | {
26 | Replace,
27 | Add,
28 | Subtract,
29 | Invert
30 | }
31 |
32 |
33 | // DOM type representing a group of effects that are applied to a region of the photo.
34 | public class EditGroup : Observable, IDisposable
35 | {
36 | public Photo Parent { get; private set; }
37 |
38 | public ObservableCollection Effects { get; } = new ObservableCollection();
39 |
40 | CanvasRenderTarget regionMask;
41 |
42 | readonly CachedImage cachedRegionMask = new CachedImage();
43 |
44 | byte[] currentRegionMask;
45 | byte[] previousRegionMask;
46 |
47 | CanvasBitmap SourceBitmap => Parent.SourceBitmap;
48 |
49 |
50 | public bool IsEnabled
51 | {
52 | get { return isEnabled; }
53 | set { SetField(ref isEnabled, value); }
54 | }
55 |
56 | bool isEnabled = true;
57 |
58 |
59 | public bool IsEditingRegion
60 | {
61 | get { return isEditingRegion; }
62 |
63 | set
64 | {
65 | SetField(ref isEditingRegion, value);
66 |
67 | // There can be only one!
68 | if (value)
69 | {
70 | foreach (var edit in Parent.Edits.Where(e => e != this))
71 | {
72 | edit.IsEditingRegion = false;
73 | }
74 | }
75 | }
76 | }
77 |
78 | bool isEditingRegion;
79 |
80 |
81 | public bool ShowRegion
82 | {
83 | get { return showRegion; }
84 | set { SetField(ref showRegion, value); }
85 | }
86 |
87 | bool showRegion = true;
88 |
89 |
90 | public SelectionMode RegionSelectionMode { get; set; }
91 | public SelectionOperation RegionSelectionOperation { get; set; }
92 |
93 |
94 | public float RegionFeather
95 | {
96 | get { return regionFeather; }
97 | set { SetField(ref regionFeather, value); }
98 | }
99 |
100 | float regionFeather;
101 |
102 |
103 | public int RegionDilate
104 | {
105 | get { return regionDilate; }
106 | set { SetField(ref regionDilate, value); }
107 | }
108 |
109 | int regionDilate;
110 |
111 |
112 | public bool CanUndo
113 | {
114 | get { return canUndo; }
115 | set { SetField(ref canUndo, value); }
116 | }
117 |
118 | bool canUndo;
119 |
120 |
121 | public EditGroup(Photo parent)
122 | {
123 | Parent = parent;
124 |
125 | Effects.CollectionChanged += (sender, e) => NotifyCollectionChanged(sender, e, "Effects");
126 |
127 | Effects.Add(new Effect(this));
128 | }
129 |
130 |
131 | public void Dispose()
132 | {
133 | Parent.Edits.Remove(this);
134 | }
135 |
136 |
137 | public ICanvasImage Apply(ICanvasImage image)
138 | {
139 | if (IsEnabled)
140 | {
141 | var originalImage = image;
142 | Rect? bounds = null;
143 |
144 | // Apply all our effects in turn.
145 | foreach (var effect in Effects)
146 | {
147 | image = effect.Apply(image, ref bounds);
148 | }
149 |
150 | // Mask so these effects only alter a specific region of the image?
151 | if (regionMask != null)
152 | {
153 | var selectedRegion = new CompositeEffect
154 | {
155 | Sources = { image, GetRegionMask() },
156 | Mode = CanvasComposite.DestinationIn
157 | };
158 |
159 | image = new CompositeEffect
160 | {
161 | Sources = { originalImage, selectedRegion }
162 | };
163 |
164 | if (bounds.HasValue)
165 | {
166 | image = new CropEffect
167 | {
168 | Source = image,
169 | SourceRectangle = bounds.Value
170 | };
171 | }
172 | }
173 | }
174 |
175 | return image;
176 | }
177 |
178 |
179 | public ICanvasImage GetRegionMask()
180 | {
181 | // Do we already have a cached version of this mask?
182 | ICanvasImage mask = cachedRegionMask.Get(regionFeather, regionDilate);
183 |
184 | if (mask == null)
185 | {
186 | mask = regionMask;
187 |
188 | // Expand or contract the selection?
189 | if (regionDilate != 0)
190 | {
191 | mask = new MorphologyEffect
192 | {
193 | Source = new BorderEffect { Source = mask },
194 | Mode = (regionDilate > 0) ? MorphologyEffectMode.Dilate : MorphologyEffectMode.Erode,
195 | Height = Math.Abs(regionDilate),
196 | Width = Math.Abs(regionDilate)
197 | };
198 | }
199 |
200 | // Feather the selection?
201 | if (regionFeather > 0)
202 | {
203 | mask = new GaussianBlurEffect
204 | {
205 | Source = mask,
206 | BlurAmount = regionFeather,
207 | BorderMode = EffectBorderMode.Hard
208 | };
209 | }
210 |
211 | // If this mask was expensive to compute, cache it now.
212 | if (mask != regionMask)
213 | {
214 | mask = cachedRegionMask.Cache(Parent, mask, regionFeather, regionDilate);
215 | }
216 | }
217 |
218 | return mask;
219 | }
220 |
221 |
222 | public void EditRegionMask(List points, float zoomFactor)
223 | {
224 | if (regionMask == null)
225 | {
226 | // Demand-create our region mask image.
227 | regionMask = new CanvasRenderTarget(SourceBitmap.Device, Parent.Size.X, Parent.Size.Y, 96);
228 | }
229 | else
230 | {
231 | // Back up the previous mask, to support undo.
232 | previousRegionMask = currentRegionMask;
233 | }
234 |
235 | // Prepare an ICanvasImage holding the edit to be applied.
236 | ICanvasImage editMask;
237 |
238 | if (RegionSelectionMode == SelectionMode.MagicWand)
239 | {
240 | // Magic wand selection is already an image.
241 | editMask = GetMagicWandMask(points, zoomFactor);
242 | }
243 | else
244 | {
245 | // Capture selection geometry into a command list.
246 | var commandList = new CanvasCommandList(regionMask.Device);
247 |
248 | using (var drawingSession = commandList.CreateDrawingSession())
249 | {
250 | // If this was just a touch without move, treat it as selecting the entire image.
251 | var dragRange = points.Select(point => Vector2.Distance(point, points[0])).Max() * zoomFactor;
252 |
253 | if (dragRange < 10)
254 | {
255 | drawingSession.Clear(Colors.White);
256 | }
257 | else
258 | {
259 | // Draw selection geometry.
260 | var geometry = GetSelectionGeometry(drawingSession, points);
261 |
262 | drawingSession.FillGeometry(geometry, Colors.White);
263 | }
264 | }
265 |
266 | editMask = commandList;
267 | }
268 |
269 | // Apply the edit.
270 | using (var drawingSession = regionMask.CreateDrawingSession())
271 | {
272 | CanvasComposite compositeMode;
273 |
274 | switch (RegionSelectionOperation)
275 | {
276 | case SelectionOperation.Replace:
277 | drawingSession.Clear(Colors.Transparent);
278 | compositeMode = CanvasComposite.SourceOver;
279 | break;
280 |
281 | case SelectionOperation.Add:
282 | compositeMode = CanvasComposite.SourceOver;
283 | break;
284 |
285 | case SelectionOperation.Subtract:
286 | compositeMode = CanvasComposite.DestinationOut;
287 | break;
288 |
289 | case SelectionOperation.Invert:
290 | compositeMode = CanvasComposite.Xor;
291 | break;
292 |
293 | default:
294 | throw new NotSupportedException();
295 | }
296 |
297 | drawingSession.DrawImage(editMask, Vector2.Zero, regionMask.Bounds, 1, CanvasImageInterpolation.Linear, compositeMode);
298 | }
299 |
300 | // Back up the mask, so we can recover from lost devices.
301 | currentRegionMask = regionMask.GetPixelBytes();
302 |
303 | cachedRegionMask.Reset();
304 |
305 | CanUndo = true;
306 | }
307 |
308 |
309 | public void UndoRegionEdit()
310 | {
311 | if (previousRegionMask != null)
312 | {
313 | regionMask.SetPixelBytes(previousRegionMask);
314 | currentRegionMask = previousRegionMask;
315 | previousRegionMask = null;
316 | }
317 | else
318 | {
319 | regionMask.Dispose();
320 | regionMask = null;
321 | currentRegionMask = null;
322 | }
323 |
324 | cachedRegionMask.Reset();
325 |
326 | CanUndo = false;
327 | }
328 |
329 |
330 | public bool DisplayRegionMask(CanvasDrawingSession drawingSession, float zoomFactor, bool editInProgress)
331 | {
332 | if (!IsEnabled || !IsEditingRegion || !ShowRegion || regionMask == null)
333 | return false;
334 |
335 | if (editInProgress && RegionSelectionOperation == SelectionOperation.Replace)
336 | return false;
337 |
338 | drawingSession.Blend = CanvasBlend.SourceOver;
339 |
340 | if (!editInProgress)
341 | {
342 | // Gray out everything outside the region.
343 | var mask = new ColorMatrixEffect
344 | {
345 | Source = GetRegionMask(),
346 |
347 | ColorMatrix = new Matrix5x4
348 | {
349 | // Set RGB = gray.
350 | M51 = 0.5f,
351 | M52 = 0.5f,
352 | M53 = 0.5f,
353 |
354 | // Invert and scale the mask alpha.
355 | M44 = -0.75f,
356 | M54 = 0.75f,
357 | }
358 | };
359 |
360 | drawingSession.DrawImage(mask);
361 | }
362 |
363 | // Magenta region border.
364 | var border = GetSelectionBorder(regionMask, zoomFactor);
365 |
366 | drawingSession.DrawImage(border);
367 |
368 | return true;
369 | }
370 |
371 |
372 | public void DisplayRegionEditInProgress(CanvasDrawingSession drawingSession, List points, float zoomFactor)
373 | {
374 | if (RegionSelectionMode == SelectionMode.MagicWand)
375 | {
376 | // Display a magic wand selection.
377 | var mask = GetMagicWandMask(points, zoomFactor);
378 | var border = GetSelectionBorder(mask, zoomFactor);
379 |
380 | drawingSession.Blend = CanvasBlend.Add;
381 | drawingSession.DrawImage(mask, Vector2.Zero, SourceBitmap.Bounds, 0.25f);
382 |
383 | drawingSession.Blend = CanvasBlend.SourceOver;
384 | drawingSession.DrawImage(border);
385 | }
386 | else
387 | {
388 | // Display a geometric shape selection.
389 | var geometry = GetSelectionGeometry(drawingSession, points);
390 |
391 | drawingSession.Blend = CanvasBlend.Add;
392 | drawingSession.FillGeometry(geometry, Color.FromArgb(0x20, 0xFF, 0xFF, 0xFF));
393 |
394 | drawingSession.Blend = CanvasBlend.SourceOver;
395 | drawingSession.DrawGeometry(geometry, Colors.Magenta, 1f / zoomFactor);
396 | }
397 | }
398 |
399 |
400 | ICanvasImage GetMagicWandMask(List points, float zoomFactor)
401 | {
402 | // What color did the user click on?
403 | Vector2 clickPoint = Vector2.Clamp(points.First(), Vector2.Zero, SourceBitmap.Size.ToVector2() - Vector2.One);
404 |
405 | Color clickColor = SourceBitmap.GetPixelColors((int)clickPoint.X, (int)clickPoint.Y, 1, 1).Single();
406 |
407 | // How far they have dragged = selection tolerance.
408 | float dragDistance = Vector2.Distance(points.First(), points.Last());
409 |
410 | float chromaTolerance = Math.Min(dragDistance / 512 * zoomFactor, 1);
411 |
412 | return new ColorMatrixEffect
413 | {
414 | Source = new ChromaKeyEffect
415 | {
416 | Source = SourceBitmap,
417 | Color = clickColor,
418 | Tolerance = chromaTolerance,
419 | InvertAlpha = true
420 | },
421 |
422 | ColorMatrix = new Matrix5x4
423 | {
424 | // Preserve alpha.
425 | M44 = 1,
426 |
427 | // Set RGB = white.
428 | M51 = 1,
429 | M52 = 1,
430 | M53 = 1,
431 | }
432 | };
433 | }
434 |
435 |
436 | ICanvasImage GetSelectionBorder(ICanvasImage mask, float zoomFactor)
437 | {
438 | // Scale so our border will always be the same width no matter how the image is zoomed.
439 | var scaleToCurrentZoom = new ScaleEffect
440 | {
441 | Source = mask,
442 | Scale = new Vector2(zoomFactor)
443 | };
444 |
445 | // Find edges of the selection.
446 | var detectEdges = new EdgeDetectionEffect
447 | {
448 | Source = scaleToCurrentZoom,
449 | Amount = 0.1f
450 | };
451 |
452 | // Colorize.
453 | var colorItMagenta = new ColorMatrixEffect
454 | {
455 | Source = detectEdges,
456 |
457 | ColorMatrix = new Matrix5x4
458 | {
459 | M11 = 1,
460 | M13 = 1,
461 | M14 = 1,
462 | }
463 | };
464 |
465 | // Scale back to the original size.
466 | return new ScaleEffect
467 | {
468 | Source = colorItMagenta,
469 | Scale = new Vector2(1 / zoomFactor)
470 | };
471 | }
472 |
473 |
474 | CanvasGeometry GetSelectionGeometry(ICanvasResourceCreator resourceCreator, List points)
475 | {
476 | Vector2 start = points.First();
477 | Vector2 end = points.Last();
478 |
479 | switch (RegionSelectionMode)
480 | {
481 | case SelectionMode.Rectangle:
482 | {
483 | Vector2 min = Vector2.Min(start, end);
484 | Vector2 size = Vector2.Abs(start - end);
485 |
486 | return CanvasGeometry.CreateRectangle(resourceCreator, min.X, min.Y, size.X, size.Y);
487 | }
488 |
489 | case SelectionMode.Ellipse:
490 | {
491 | Vector2 center = (start + end) / 2;
492 | Vector2 radius = Vector2.Abs(start - end) / 2;
493 |
494 | return CanvasGeometry.CreateEllipse(resourceCreator, center, radius.X, radius.Y);
495 | }
496 |
497 | case SelectionMode.Freehand:
498 | {
499 | return CanvasGeometry.CreatePolygon(resourceCreator, points.ToArray());
500 | }
501 |
502 | default:
503 | throw new NotSupportedException();
504 | }
505 | }
506 |
507 |
508 | public void RecoverAfterDeviceLost()
509 | {
510 | if (regionMask != null)
511 | {
512 | regionMask.Dispose();
513 | }
514 |
515 | if (currentRegionMask != null)
516 | {
517 | regionMask = new CanvasRenderTarget(SourceBitmap.Device, Parent.Size.X, Parent.Size.Y, 96);
518 | regionMask.SetPixelBytes(currentRegionMask);
519 | }
520 |
521 | cachedRegionMask.RecoverAfterDeviceLost();
522 | }
523 |
524 |
525 | public void SaveSuspendedState(BinaryWriter writer)
526 | {
527 | writer.Write(IsEnabled);
528 | writer.Write(IsEditingRegion);
529 | writer.Write(ShowRegion);
530 | writer.Write((int)RegionSelectionMode);
531 | writer.Write((int)RegionSelectionOperation);
532 | writer.Write(RegionFeather);
533 | writer.Write(RegionDilate);
534 | writer.Write(CanUndo);
535 |
536 | writer.WriteByteArray(currentRegionMask);
537 | writer.WriteByteArray(previousRegionMask);
538 |
539 | writer.WriteCollection(Effects, effect => effect.SaveSuspendedState(writer));
540 | }
541 |
542 |
543 | public static EditGroup RestoreSuspendedState(Photo parent, BinaryReader reader)
544 | {
545 | var group = new EditGroup(parent);
546 |
547 | group.IsEnabled = reader.ReadBoolean();
548 | group.IsEditingRegion = reader.ReadBoolean();
549 | group.ShowRegion = reader.ReadBoolean();
550 | group.RegionSelectionMode = (SelectionMode)reader.ReadInt32();
551 | group.RegionSelectionOperation = (SelectionOperation)reader.ReadInt32();
552 | group.RegionFeather = reader.ReadSingle();
553 | group.RegionDilate = reader.ReadInt32();
554 | group.CanUndo = reader.ReadBoolean();
555 |
556 | group.currentRegionMask = reader.ReadByteArray();
557 | group.previousRegionMask = reader.ReadByteArray();
558 |
559 | reader.ReadCollection(group.Effects, () => Effect.RestoreSuspendedState(group, reader));
560 |
561 | return group;
562 | }
563 | }
564 | }
565 |
--------------------------------------------------------------------------------
/Stuart/MainPage.xaml.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Graphics.Canvas;
2 | using Microsoft.Graphics.Canvas.UI;
3 | using Microsoft.Graphics.Canvas.UI.Xaml;
4 | using System;
5 | using System.Collections.Generic;
6 | using System.ComponentModel;
7 | using System.IO;
8 | using System.Linq;
9 | using System.Numerics;
10 | using System.Threading.Tasks;
11 | using Windows.ApplicationModel;
12 | using Windows.ApplicationModel.Activation;
13 | using Windows.ApplicationModel.DataTransfer;
14 | using Windows.Foundation.Metadata;
15 | using Windows.Storage;
16 | using Windows.Storage.AccessCache;
17 | using Windows.Storage.Pickers;
18 | using Windows.System;
19 | using Windows.UI.Input;
20 | using Windows.UI.Popups;
21 | using Windows.UI.ViewManagement;
22 | using Windows.UI.Xaml;
23 | using Windows.UI.Xaml.Controls;
24 | using Windows.UI.Xaml.Input;
25 | using Windows.UI.Xaml.Navigation;
26 |
27 | namespace Stuart
28 | {
29 | // UI codebehind for the main application page.
30 | public sealed partial class MainPage : Page
31 | {
32 | Photo photo = new Photo();
33 |
34 | StorageFile currentFile;
35 |
36 | EditGroup editingRegion;
37 | readonly List regionPoints = new List();
38 |
39 | readonly CachedImage cachedImage = new CachedImage();
40 |
41 | float? lastDrawnZoomFactor;
42 |
43 | object launchArg;
44 |
45 | const string suspensionSerializationFile = "stuart.appstate";
46 |
47 |
48 | static readonly List imageFileExtensions = new List
49 | {
50 | ".jpg",
51 | ".jpeg",
52 | ".png"
53 | };
54 |
55 |
56 | public MainPage()
57 | {
58 | this.InitializeComponent();
59 |
60 | DataContext = photo;
61 |
62 | photo.PropertyChanged += Photo_PropertyChanged;
63 |
64 | Application.Current.Suspending += OnSuspending;
65 |
66 | // Hide the status bar.
67 | if (ApiInformation.IsTypePresent("Windows.UI.ViewManagement.StatusBar"))
68 | {
69 | var action = StatusBar.GetForCurrentView().HideAsync();
70 | }
71 | }
72 |
73 |
74 | protected override void OnNavigatedTo(NavigationEventArgs e)
75 | {
76 | launchArg = e.Parameter;
77 |
78 | base.OnNavigatedTo(e);
79 | }
80 |
81 |
82 | void canvas_CreateResources(CanvasControl sender, CanvasCreateResourcesEventArgs args)
83 | {
84 | switch (args.Reason)
85 | {
86 | case CanvasCreateResourcesReason.FirstTime:
87 | // First time initialization: either restore suspended app state, load a
88 | // photo that was passed in from the shell, or bring up the file selector.
89 | if (launchArg is ApplicationExecutionState && (ApplicationExecutionState)launchArg == ApplicationExecutionState.Terminated)
90 | {
91 | var restoreTask = RestoreSuspendedState(sender.Device);
92 |
93 | args.TrackAsyncAction(restoreTask.AsAsyncAction());
94 | }
95 | else
96 | {
97 | if (!TryLoadPhoto(launchArg as IReadOnlyList))
98 | {
99 | LoadButton_Click(null, null);
100 | }
101 | }
102 | break;
103 |
104 | case CanvasCreateResourcesReason.NewDevice:
105 | // Recovering after a lost device (GPU reset).
106 | if (photo.SourceBitmap != null)
107 | {
108 | photo.RecoverAfterDeviceLost(sender.Device);
109 | }
110 |
111 | cachedImage.RecoverAfterDeviceLost();
112 | break;
113 |
114 | case CanvasCreateResourcesReason.DpiChanged:
115 | // We mostly work in pixels rather than DIPs, so only need
116 | // minimal layout updates in response to DPI changes.
117 | if (photo.SourceBitmap != null)
118 | {
119 | ZoomToFitPhoto();
120 | }
121 | break;
122 | }
123 | }
124 |
125 |
126 | public bool TryLoadPhoto(IReadOnlyList storageItems)
127 | {
128 | if (storageItems == null)
129 | return false;
130 |
131 | var file = GetSingleImageFile(storageItems);
132 |
133 | if (file == null)
134 | return false;
135 |
136 | LoadPhoto(file);
137 |
138 | return true;
139 | }
140 |
141 |
142 | async void LoadButton_Click(object sender, RoutedEventArgs e)
143 | {
144 | var picker = new FileOpenPicker
145 | {
146 | ViewMode = PickerViewMode.Thumbnail,
147 | SuggestedStartLocation = PickerLocationId.PicturesLibrary
148 | };
149 |
150 | imageFileExtensions.ForEach(picker.FileTypeFilter.Add);
151 |
152 | StorageFile file;
153 |
154 | // On Phone, PickSingleFileAsync throws if we call it immediately on page load.
155 | // Let's just catch that, wait a bit, then try again.
156 | retryAfterBogusFailure:
157 |
158 | try
159 | {
160 | file = await picker.PickSingleFileAsync();
161 | }
162 | catch (UnauthorizedAccessException)
163 | {
164 | await Task.Delay(100);
165 |
166 | goto retryAfterBogusFailure;
167 | }
168 |
169 | if (file != null)
170 | {
171 | LoadPhoto(file);
172 | }
173 | }
174 |
175 |
176 | void SaveButton_Click(object sender, RoutedEventArgs e)
177 | {
178 | SavePhoto(currentFile);
179 | }
180 |
181 |
182 | async void SaveAsButton_Click(object sender, RoutedEventArgs e)
183 | {
184 | var picker = new FileSavePicker
185 | {
186 | SuggestedSaveFile = currentFile,
187 | SuggestedStartLocation = PickerLocationId.PicturesLibrary,
188 | DefaultFileExtension = imageFileExtensions[0],
189 | FileTypeChoices = { { "Image files", imageFileExtensions } }
190 | };
191 |
192 | var file = await picker.PickSaveFileAsync();
193 |
194 | if (file != null)
195 | {
196 | SavePhoto(file);
197 | }
198 | }
199 |
200 |
201 | async void LoadPhoto(StorageFile file)
202 | {
203 | try
204 | {
205 | await photo.Load(canvas.Device, file);
206 |
207 | currentFile = file;
208 |
209 | ZoomToFitPhoto();
210 | }
211 | catch (Exception exception)
212 | {
213 | string message = "Error loading photo.";
214 |
215 | if (exception is ArgumentException)
216 | {
217 | message += "\n\nThis image is too high a resolution for your GPU to load into a single texture. " +
218 | "And this app is not sophisticated enough to split it into multiple smaller textures. " +
219 | "Sorry!";
220 | }
221 |
222 | await new MessageDialog(message).ShowAsync();
223 |
224 | if (canvas.Device.IsDeviceLost(exception.HResult))
225 | {
226 | canvas.Device.RaiseDeviceLost();
227 | }
228 | }
229 | }
230 |
231 |
232 | async void SavePhoto(StorageFile file)
233 | {
234 | try
235 | {
236 | await photo.Save(file);
237 |
238 | currentFile = file;
239 | }
240 | catch (Exception exception)
241 | {
242 | await new MessageDialog("Error saving photo").ShowAsync();
243 |
244 | if (canvas.Device.IsDeviceLost(exception.HResult))
245 | {
246 | canvas.Device.RaiseDeviceLost();
247 | }
248 | }
249 | }
250 |
251 |
252 | void Page_DragEnter(object sender, DragEventArgs e)
253 | {
254 | HandleDrop(e, file =>
255 | {
256 | e.AcceptedOperation = DataPackageOperation.Move;
257 | e.DragUIOverride.IsCaptionVisible = false;
258 | });
259 | }
260 |
261 |
262 | void Page_Drop(object sender, DragEventArgs e)
263 | {
264 | HandleDrop(e, file =>
265 | {
266 | LoadPhoto(file);
267 | });
268 | }
269 |
270 |
271 | static async void HandleDrop(DragEventArgs e, Action handleDroppedFile)
272 | {
273 | if (!e.DataView.Contains(StandardDataFormats.StorageItems))
274 | return;
275 |
276 | var deferral = e.GetDeferral();
277 |
278 | try
279 | {
280 | var storageItems = await e.DataView.GetStorageItemsAsync();
281 |
282 | var file = GetSingleImageFile(storageItems);
283 |
284 | if (file != null)
285 | {
286 | handleDroppedFile(file);
287 |
288 | e.Handled = true;
289 | }
290 | }
291 | finally
292 | {
293 | deferral.Complete();
294 | }
295 | }
296 |
297 |
298 | static StorageFile GetSingleImageFile(IReadOnlyList storageItems)
299 | {
300 | var files = storageItems.OfType().ToList();
301 |
302 | if (files.Any(file => !imageFileExtensions.Contains(file.FileType, StringComparer.OrdinalIgnoreCase)))
303 | return null;
304 |
305 | if (files.Count() != 1)
306 | return null;
307 |
308 | return files.Single();
309 | }
310 |
311 |
312 | async void OnSuspending(object sender, SuspendingEventArgs e)
313 | {
314 | var deferral = e.SuspendingOperation.GetDeferral();
315 |
316 | try
317 | {
318 | var file = await ApplicationData.Current.LocalFolder.CreateFileAsync(suspensionSerializationFile, CreationCollisionOption.ReplaceExisting);
319 |
320 | if (currentFile != null)
321 | {
322 | // Persist our state.
323 | using (var stream = await file.OpenAsync(FileAccessMode.ReadWrite))
324 | using (var writer = new BinaryWriter(stream.AsStreamForWrite()))
325 | {
326 | writer.Write(currentFile.Path);
327 |
328 | photo.SaveSuspendedState(writer);
329 | }
330 |
331 | // Persist permissions to later go back to this same file.
332 | var futureAccessList = StorageApplicationPermissions.FutureAccessList;
333 |
334 | futureAccessList.Clear();
335 | futureAccessList.Add(currentFile);
336 | }
337 | }
338 | finally
339 | {
340 | deferral.Complete();
341 | }
342 | }
343 |
344 |
345 | async Task RestoreSuspendedState(CanvasDevice device)
346 | {
347 | try
348 | {
349 | var file = await ApplicationData.Current.LocalFolder.CreateFileAsync(suspensionSerializationFile, CreationCollisionOption.OpenIfExists);
350 |
351 | using (var stream = await file.OpenAsync(FileAccessMode.Read))
352 | using (var reader = new BinaryReader(stream.AsStreamForRead()))
353 | {
354 | var photoFilename = reader.ReadString();
355 |
356 | currentFile = await StorageFile.GetFileFromPathAsync(photoFilename);
357 |
358 | photo.RestoreSuspendedState(device, reader);
359 |
360 | ZoomToFitPhoto();
361 | }
362 | }
363 | catch { }
364 | }
365 |
366 |
367 | void ZoomToFitPhoto()
368 | {
369 | // Convert the photo size from pixels to dips.
370 | var photoSize = photo.Size;
371 |
372 | photoSize.X = canvas.ConvertPixelsToDips((int)photoSize.X);
373 | photoSize.Y = canvas.ConvertPixelsToDips((int)photoSize.Y);
374 |
375 | // Size the CanvasControl to exactly fit the image.
376 | canvas.Width = photoSize.X;
377 | canvas.Height = photoSize.Y;
378 |
379 | // Zoom so the whole image is visible.
380 | var viewSize = new Vector2((float)scrollView.ActualWidth, (float)scrollView.ActualHeight);
381 | var sizeRatio = viewSize / photoSize;
382 | var zoomFactor = Math.Min(sizeRatio.X, sizeRatio.Y) * 0.95f;
383 |
384 | scrollView.ChangeView(0, 0, zoomFactor);
385 | }
386 |
387 |
388 | void Canvas_Draw(CanvasControl sender, CanvasDrawEventArgs args)
389 | {
390 | if (photo.SourceBitmap == null)
391 | return;
392 |
393 | var drawingSession = args.DrawingSession;
394 |
395 | drawingSession.Units = CanvasUnits.Pixels;
396 | drawingSession.Blend = CanvasBlend.Copy;
397 |
398 | // Draw the main photo image.
399 | ICanvasImage image;
400 |
401 | if (editingRegion != null)
402 | {
403 | image = cachedImage.Get() ?? cachedImage.Cache(photo, photo.GetImage());
404 | }
405 | else
406 | {
407 | image = photo.GetImage();
408 | }
409 |
410 | drawingSession.DrawImage(image);
411 |
412 | // Highlight the current region (if any).
413 | lastDrawnZoomFactor = null;
414 |
415 | foreach (var edit in photo.Edits)
416 | {
417 | if (edit.DisplayRegionMask(drawingSession, scrollView.ZoomFactor, editingRegion != null))
418 | {
419 | lastDrawnZoomFactor = scrollView.ZoomFactor;
420 | }
421 | }
422 |
423 | // Display any in-progress region edits.
424 | if (editingRegion != null)
425 | {
426 | editingRegion.DisplayRegionEditInProgress(drawingSession, regionPoints, scrollView.ZoomFactor);
427 | }
428 | }
429 |
430 |
431 | void Canvas_PointerPressed(object sender, PointerRoutedEventArgs e)
432 | {
433 | if (editingRegion != null)
434 | return;
435 |
436 | // If any of the edit groups is in edit region mode, set that as our current region.
437 | editingRegion = photo.Edits.SingleOrDefault(edit => edit.IsEditingRegion);
438 |
439 | if (editingRegion == null)
440 | return;
441 |
442 | // Add the start point.
443 | regionPoints.Add(ConvertDipsToPixels(e.GetCurrentPoint(canvas)));
444 |
445 | // Set the manipulation mode so we grab all input, bypassing our parent ScrollViewer.
446 | canvas.ManipulationMode = ManipulationModes.All;
447 | canvas.CapturePointer(e.Pointer);
448 |
449 | cachedImage.Reset();
450 |
451 | canvas.Invalidate();
452 | e.Handled = true;
453 | }
454 |
455 |
456 | void Canvas_PointerMoved(object sender, PointerRoutedEventArgs e)
457 | {
458 | if (editingRegion == null)
459 | return;
460 |
461 | // Add points to the edit region.
462 | regionPoints.AddRange(from point in e.GetIntermediatePoints(canvas)
463 | select ConvertDipsToPixels(point));
464 |
465 | canvas.Invalidate();
466 | e.Handled = true;
467 | }
468 |
469 |
470 | void Canvas_PointerReleased(object sender, PointerRoutedEventArgs e)
471 | {
472 | if (editingRegion == null)
473 | return;
474 |
475 | try
476 | {
477 | editingRegion.EditRegionMask(regionPoints, scrollView.ZoomFactor);
478 | }
479 | catch (Exception exception) when (canvas.Device.IsDeviceLost(exception.HResult))
480 | {
481 | canvas.Device.RaiseDeviceLost();
482 | }
483 |
484 | editingRegion = null;
485 | regionPoints.Clear();
486 |
487 | // Restore the manipulation mode so input goes to the parent ScrollViewer again.
488 | canvas.ManipulationMode = ManipulationModes.System;
489 | canvas.ReleasePointerCapture(e.Pointer);
490 |
491 | canvas.Invalidate();
492 | e.Handled = true;
493 | }
494 |
495 |
496 | void Page_KeyDown(object sender, KeyRoutedEventArgs e)
497 | {
498 | if (e.Key == VirtualKey.A || e.Key == VirtualKey.Z)
499 | {
500 | var currentZoom = scrollView.ZoomFactor;
501 | var newZoom = currentZoom;
502 |
503 | if (e.Key == VirtualKey.A)
504 | newZoom /= 0.9f;
505 | else
506 | newZoom *= 0.9f;
507 |
508 | newZoom = Math.Max(newZoom, scrollView.MinZoomFactor);
509 | newZoom = Math.Min(newZoom, scrollView.MaxZoomFactor);
510 |
511 | var currentPan = new Vector2((float)scrollView.HorizontalOffset,
512 | (float)scrollView.VerticalOffset);
513 |
514 | var centerOffset = new Vector2((float)scrollView.ViewportWidth,
515 | (float)scrollView.ViewportHeight) / 2;
516 |
517 | var newPan = ((currentPan + centerOffset) * newZoom / currentZoom) - centerOffset;
518 |
519 | scrollView.ChangeView(newPan.X, newPan.Y, newZoom);
520 |
521 | e.Handled = true;
522 | }
523 | }
524 |
525 |
526 | void Photo_PropertyChanged(object sender, PropertyChangedEventArgs e)
527 | {
528 | switch (e.PropertyName)
529 | {
530 | case "SelectedEffect":
531 | break;
532 |
533 | default:
534 | canvas.Invalidate();
535 | break;
536 | }
537 | }
538 |
539 |
540 | void ScrollView_ViewChanged(object sender, ScrollViewerViewChangedEventArgs e)
541 | {
542 | if (!e.IsIntermediate &&
543 | lastDrawnZoomFactor.HasValue &&
544 | lastDrawnZoomFactor != scrollView.ZoomFactor)
545 | {
546 | canvas.Invalidate();
547 | }
548 | }
549 |
550 |
551 | void NewEdit_Click(object sender, RoutedEventArgs e)
552 | {
553 | photo.Edits.Add(new EditGroup(photo));
554 | }
555 |
556 |
557 | void Background_PointerPressed(object sender, PointerRoutedEventArgs e)
558 | {
559 | photo.SelectedEffect = null;
560 | }
561 |
562 |
563 | void HelpButton_Click(object sender, RoutedEventArgs e)
564 | {
565 | const string url = "http://github.com/shawnhar/stuart/blob/master/README.md";
566 |
567 | var operation = Launcher.LaunchUriAsync(new Uri(url));
568 | }
569 |
570 |
571 | void PrivacyButton_Click(object sender, RoutedEventArgs e)
572 | {
573 | const string url = "http://github.com/shawnhar/stuart/blob/master/PRIVACY.txt";
574 |
575 | var operation = Launcher.LaunchUriAsync(new Uri(url));
576 | }
577 |
578 |
579 | float ConvertDipsToPixels(float value)
580 | {
581 | return value * canvas.Dpi / 96;
582 | }
583 |
584 |
585 | Vector2 ConvertDipsToPixels(PointerPoint point)
586 | {
587 | var value = point.Position.ToVector2();
588 |
589 | return new Vector2(ConvertDipsToPixels(value.X),
590 | ConvertDipsToPixels(value.Y));
591 | }
592 | }
593 | }
594 |
--------------------------------------------------------------------------------
/Stuart/Package.StoreAssociation.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | CN=10FD0F0A-75DA-4756-83C0-3156C7AF495F
4 | Shawn Hargreaves
5 | http://www.w3.org/2001/04/xmlenc#sha256
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 |
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 |
320 |
321 |
322 |
323 |
324 |
325 |
326 |
327 |
328 |
329 |
330 |
331 |
332 |
333 |
334 |
335 |
336 |
337 |
338 |
339 |
340 |
341 |
342 |
343 |
344 |
345 |
346 |
347 |
348 |
349 |
350 |
351 | 35642ShawnHargreaves.Stuart
352 |
353 | Stuart
354 | Stuart Photo Editor
355 |
356 |
357 |
358 | 35642ShawnHargreaves.WhichBridgeOverLakeWashington
359 | 35642ShawnHargreaves.Win2DExampleGallery
360 |
361 |
362 |
--------------------------------------------------------------------------------