From 79cbb8f246c82f0c4f549719c2dd05ebb861f2c6 Mon Sep 17 00:00:00 2001 From: Kevin VanGelder Date: Mon, 21 Nov 2016 17:48:39 -0800 Subject: [PATCH] Re-implement image w/o submodule changes --- .../Modules/Image/BitmapImageHelpers.cs | 112 ++++++++++++++++++ .../Modules/Image/ImageLoaderModule.cs | 73 ++++++++++++ .../ReactNative.Net46.csproj | 2 + .../Shell/MainReactPackage.cs | 6 +- .../Modules/Image/ImageLoadStatus.cs | 0 .../Modules/Image/ImageMetadata.cs | 0 .../Modules/Image/ImageStatusEventData.cs | 0 .../ReactNative.Shared.projitems | 5 + .../Views/Image/ReactImageLoadEvent.cs | 0 .../Views/Image/ReactImageManager.cs | 22 +++- ReactWindows/ReactNative/ReactNative.csproj | 5 - 11 files changed, 217 insertions(+), 8 deletions(-) create mode 100644 ReactWindows/ReactNative.Net46/Modules/Image/BitmapImageHelpers.cs create mode 100644 ReactWindows/ReactNative.Net46/Modules/Image/ImageLoaderModule.cs rename ReactWindows/{ReactNative => ReactNative.Shared}/Modules/Image/ImageLoadStatus.cs (100%) rename ReactWindows/{ReactNative => ReactNative.Shared}/Modules/Image/ImageMetadata.cs (100%) rename ReactWindows/{ReactNative => ReactNative.Shared}/Modules/Image/ImageStatusEventData.cs (100%) rename ReactWindows/{ReactNative => ReactNative.Shared}/Views/Image/ReactImageLoadEvent.cs (100%) rename ReactWindows/{ReactNative => ReactNative.Shared}/Views/Image/ReactImageManager.cs (96%) diff --git a/ReactWindows/ReactNative.Net46/Modules/Image/BitmapImageHelpers.cs b/ReactWindows/ReactNative.Net46/Modules/Image/BitmapImageHelpers.cs new file mode 100644 index 00000000000..3081b30db06 --- /dev/null +++ b/ReactWindows/ReactNative.Net46/Modules/Image/BitmapImageHelpers.cs @@ -0,0 +1,112 @@ +using System; +using System.IO; +using System.Net; +using System.Reactive; +using System.Reactive.Concurrency; +using System.Reactive.Linq; +using System.Windows; +using System.Windows.Media.Imaging; +using static System.FormattableString; + +namespace ReactNative.Modules.Image +{ + static class BitmapImageHelpers + { + public static bool IsBase64Uri(string uri) + { + if (uri == null) + { + throw new ArgumentNullException(nameof(uri)); + } + + return uri.StartsWith("data:"); + } + + public static Stream GetStreamAsync(string uri) + { + if (uri == null) + { + throw new ArgumentNullException(nameof(uri)); + } + + if (IsBase64Uri(uri)) + { + var decodedData = Convert.FromBase64String(uri.Substring(uri.IndexOf(",") + 1)); + return new MemoryStream(decodedData); + } + else + { + var uriValue = default(Uri); + if (!Uri.TryCreate(uri, UriKind.Absolute, out uriValue)) + { + throw new ArgumentOutOfRangeException(nameof(uri), Invariant($"Invalid URI '{uri}' provided.")); + } + + var streamReference = new StreamReader(WebRequest.Create(uri).GetResponse().GetResponseStream()); //RandomAccessStreamReference.CreateFromUri(uriValue); + return streamReference.BaseStream; + } + } + + public static IObservable GetStreamLoadObservable(this BitmapImage image) + { + return image.GetOpenedObservable() + .Merge(image.GetFailedObservable(), Scheduler.Default) + .StartWith(new ImageStatusEventData(ImageLoadStatus.OnLoadStart)); + } + + public static IObservable GetUriLoadObservable(this BitmapImage image) + { + return Observable.Merge( + Scheduler.Default, + image.GetDownloadingObservable(), + image.GetOpenedObservable(), + image.GetFailedObservable()); + } + + private static IObservable GetOpenedObservable(this BitmapImage image) + { + return Observable.FromEventPattern( + h => image.DownloadCompleted += h, + h => image.DownloadCompleted -= h) + .Select(args => + { + var bitmapImage = args.Sender as BitmapImage; + if (bitmapImage != null) + { + return new ImageStatusEventData( + ImageLoadStatus.OnLoad, + new ImageMetadata( + image.UriSource?.AbsoluteUri, + image.PixelWidth, + image.PixelHeight)); + } + else + { + return new ImageStatusEventData(ImageLoadStatus.OnLoad); + } + }) + .Take(1) + .Concat(Observable.Return(new ImageStatusEventData(ImageLoadStatus.OnLoadEnd))); + } + + private static IObservable GetFailedObservable(this BitmapImage image) + { + return Observable.FromEventPattern, ExceptionRoutedEventArgs>( + h => image.DownloadFailed += h, + h => image.DownloadFailed -= h) + .Select, ImageStatusEventData>(pattern => + { + throw new InvalidOperationException(pattern.EventArgs.ErrorException.Message); + }); + } + + private static IObservable GetDownloadingObservable(this BitmapImage image) + { + return Observable.FromEventPattern, DownloadProgressChangedEventArgs>( + h => image.DownloadProgress += h, + h => image.DownloadProgress -= h) + .Take(1) + .Select(_ => new ImageStatusEventData(ImageLoadStatus.OnLoadStart)); + } + } +} diff --git a/ReactWindows/ReactNative.Net46/Modules/Image/ImageLoaderModule.cs b/ReactWindows/ReactNative.Net46/Modules/Image/ImageLoaderModule.cs new file mode 100644 index 00000000000..44ecbf680f1 --- /dev/null +++ b/ReactWindows/ReactNative.Net46/Modules/Image/ImageLoaderModule.cs @@ -0,0 +1,73 @@ +using Newtonsoft.Json.Linq; +using ReactNative.Bridge; +using System; +using System.Reactive.Linq; +using System.Windows.Media.Imaging; + +namespace ReactNative.Modules.Image +{ + class ImageLoaderModule : NativeModuleBase + { + private const string ErrorInvalidUri = "E_INVALID_URI"; + private const string ErrorPrefetchFailure = "E_PREFETCH_FAILURE"; + private const string ErrorGetSizeFailure = "E_GET_SIZE_FAILURE"; + + public override string Name + { + get + { + return "ImageLoader"; + } + } + + [ReactMethod] + public void prefetchImage(string uriString, IPromise promise) + { + promise.Reject(ErrorPrefetchFailure, "Prefetch is not yet implemented."); + } + + [ReactMethod] + public void getSize(string uriString, IPromise promise) + { + if (string.IsNullOrEmpty(uriString)) + { + promise.Reject(ErrorInvalidUri, "Cannot get the size of an image for an empty URI."); + return; + } + + DispatcherHelpers.RunOnDispatcher(async () => + { + try + { + var bitmapImage = new BitmapImage(); + bitmapImage.BeginInit(); + var loadQuery = bitmapImage.GetStreamLoadObservable() + .Where(status => status.LoadStatus == ImageLoadStatus.OnLoadEnd) + .FirstAsync() + .Replay(1); + + using (loadQuery.Connect()) + { + using (var stream = BitmapImageHelpers.GetStreamAsync(uriString)) + { + bitmapImage.StreamSource = stream; + } + + await loadQuery; + bitmapImage.EndInit(); + + promise.Resolve(new JObject + { + { "width", bitmapImage.PixelWidth }, + { "height", bitmapImage.PixelHeight }, + }); + } + } + catch (Exception ex) + { + promise.Reject(ErrorGetSizeFailure, ex.Message); + } + }); + } + } +} diff --git a/ReactWindows/ReactNative.Net46/ReactNative.Net46.csproj b/ReactWindows/ReactNative.Net46/ReactNative.Net46.csproj index e481f5d8726..affffe3026c 100644 --- a/ReactWindows/ReactNative.Net46/ReactNative.Net46.csproj +++ b/ReactWindows/ReactNative.Net46/ReactNative.Net46.csproj @@ -134,6 +134,8 @@ + + diff --git a/ReactWindows/ReactNative.Net46/Shell/MainReactPackage.cs b/ReactWindows/ReactNative.Net46/Shell/MainReactPackage.cs index 58caff6a041..6d2c24c1eb3 100644 --- a/ReactWindows/ReactNative.Net46/Shell/MainReactPackage.cs +++ b/ReactWindows/ReactNative.Net46/Shell/MainReactPackage.cs @@ -3,12 +3,14 @@ using ReactNative.Modules.Clipboard; using ReactNative.Modules.Core; using ReactNative.Modules.Dialog; +using ReactNative.Modules.Image; using ReactNative.Modules.I18N; using ReactNative.Modules.NetInfo; using ReactNative.Modules.Network; using ReactNative.Modules.Storage; using ReactNative.Modules.WebSocket; using ReactNative.UIManager; +using ReactNative.Views.Image; using ReactNative.Views.Text; using ReactNative.Views.View; using System; @@ -36,7 +38,7 @@ public IReadOnlyList CreateNativeModules(ReactContext reactContex //new CameraRollManager(reactContext), new ClipboardModule(), new DialogModule(reactContext), - //new ImageLoaderModule(), + new ImageLoaderModule(), new I18NModule(), //new LauncherModule(reactContext), //new LocationModule(reactContext), @@ -71,7 +73,7 @@ public IReadOnlyList CreateViewManagers( return new List { //new ReactFlipViewManager(), - //new ReactImageManager(), + new ReactImageManager(), //new ReactProgressBarViewManager(), //new ReactProgressRingViewManager(), //new ReactPickerManager(), diff --git a/ReactWindows/ReactNative/Modules/Image/ImageLoadStatus.cs b/ReactWindows/ReactNative.Shared/Modules/Image/ImageLoadStatus.cs similarity index 100% rename from ReactWindows/ReactNative/Modules/Image/ImageLoadStatus.cs rename to ReactWindows/ReactNative.Shared/Modules/Image/ImageLoadStatus.cs diff --git a/ReactWindows/ReactNative/Modules/Image/ImageMetadata.cs b/ReactWindows/ReactNative.Shared/Modules/Image/ImageMetadata.cs similarity index 100% rename from ReactWindows/ReactNative/Modules/Image/ImageMetadata.cs rename to ReactWindows/ReactNative.Shared/Modules/Image/ImageMetadata.cs diff --git a/ReactWindows/ReactNative/Modules/Image/ImageStatusEventData.cs b/ReactWindows/ReactNative.Shared/Modules/Image/ImageStatusEventData.cs similarity index 100% rename from ReactWindows/ReactNative/Modules/Image/ImageStatusEventData.cs rename to ReactWindows/ReactNative.Shared/Modules/Image/ImageStatusEventData.cs diff --git a/ReactWindows/ReactNative.Shared/ReactNative.Shared.projitems b/ReactWindows/ReactNative.Shared/ReactNative.Shared.projitems index 5ff93fb7a72..95e836377be 100644 --- a/ReactWindows/ReactNative.Shared/ReactNative.Shared.projitems +++ b/ReactWindows/ReactNative.Shared/ReactNative.Shared.projitems @@ -115,6 +115,9 @@ + + + @@ -190,6 +193,8 @@ + + diff --git a/ReactWindows/ReactNative/Views/Image/ReactImageLoadEvent.cs b/ReactWindows/ReactNative.Shared/Views/Image/ReactImageLoadEvent.cs similarity index 100% rename from ReactWindows/ReactNative/Views/Image/ReactImageLoadEvent.cs rename to ReactWindows/ReactNative.Shared/Views/Image/ReactImageLoadEvent.cs diff --git a/ReactWindows/ReactNative/Views/Image/ReactImageManager.cs b/ReactWindows/ReactNative.Shared/Views/Image/ReactImageManager.cs similarity index 96% rename from ReactWindows/ReactNative/Views/Image/ReactImageManager.cs rename to ReactWindows/ReactNative.Shared/Views/Image/ReactImageManager.cs index 13a27573aa9..57d781c636f 100644 --- a/ReactWindows/ReactNative/Views/Image/ReactImageManager.cs +++ b/ReactWindows/ReactNative.Shared/Views/Image/ReactImageManager.cs @@ -6,10 +6,17 @@ using System; using System.Collections.Generic; using System.Reactive.Disposables; +#if WINDOWS_UWP using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Media; using Windows.UI.Xaml.Media.Imaging; +#else +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using System.Windows.Media.Imaging; +#endif namespace ReactNative.Views.Image { @@ -287,16 +294,25 @@ private async void SetUriFromSingleSource(Border view, string source) } var image = new BitmapImage(); +#if !WINDOWS_UWP + image.BeginInit(); +#endif if (BitmapImageHelpers.IsBase64Uri(source)) { disposable.Disposable = image.GetStreamLoadObservable().Subscribe( status => OnImageStatusUpdate(view, status), _ => OnImageFailed(view)); - +#if WINDOWS_UWP using (var stream = await BitmapImageHelpers.GetStreamAsync(source)) { await image.SetSourceAsync(stream); } +#else + using (var stream = BitmapImageHelpers.GetStreamAsync(source)) + { + image.StreamSource = stream; + } +#endif } else { @@ -307,6 +323,10 @@ private async void SetUriFromSingleSource(Border view, string source) image.UriSource = new Uri(source); } +#if !WINDOWS_UWP + image.EndInit(); +#endif + imageBrush.ImageSource = image; } diff --git a/ReactWindows/ReactNative/ReactNative.csproj b/ReactWindows/ReactNative/ReactNative.csproj index 0724f4268cc..56db1b55f7e 100644 --- a/ReactWindows/ReactNative/ReactNative.csproj +++ b/ReactWindows/ReactNative/ReactNative.csproj @@ -124,9 +124,6 @@ - - - @@ -166,8 +163,6 @@ - -