Implement ImageRenderer (fixes #44)

This commit is contained in:
Frank A. Krueger 2017-12-10 17:51:05 -08:00
parent 8a27f336bd
commit 4573596464
5 changed files with 312 additions and 12 deletions

View File

@ -25,10 +25,13 @@ namespace Microsoft.AspNetCore.Builder
};
app.UseWebSockets (webSocketOptions);
Ooui.UI.ServerEnabled = false;
app.Use (async (context, next) =>
{
var response = context.Response;
if (context.Request.Path == jsPath) {
var response = context.Response;
var clientJsBytes = Ooui.UI.ClientJsBytes;
var clientJsEtag = Ooui.UI.ClientJsEtag;
if (context.Request.Headers.TryGetValue ("If-None-Match", out var inms) && inms.Count > 0 && inms[0] == clientJsEtag) {
@ -53,6 +56,21 @@ namespace Microsoft.AspNetCore.Builder
context.Response.StatusCode = 400;
}
}
else if (Ooui.UI.TryGetFileContentAtPath (context.Request.Path, out var file)) {
if (context.Request.Headers.TryGetValue ("If-None-Match", out var inms) && inms.Count > 0 && inms[0] == file.Etag) {
response.StatusCode = 304;
}
else {
response.StatusCode = 200;
response.ContentLength = file.Content.Length;
response.ContentType = file.ContentType;
response.Headers.Add ("Cache-Control", "public, max-age=60");
response.Headers.Add ("Etag", file.Etag);
using (var s = response.Body) {
await s.WriteAsync (file.Content, 0, file.Content.Length).ConfigureAwait (false);
}
}
}
else {
await next ().ConfigureAwait (false);
}

View File

@ -12,9 +12,13 @@ using Xamarin.Forms.Internals;
[assembly: ExportRenderer (typeof (Editor), typeof (EditorRenderer))]
[assembly: ExportRenderer (typeof (Entry), typeof (EntryRenderer))]
[assembly: ExportRenderer (typeof (Frame), typeof (FrameRenderer))]
[assembly: ExportRenderer (typeof (Image), typeof (ImageRenderer))]
[assembly: ExportRenderer (typeof (Label), typeof (LabelRenderer))]
[assembly: ExportRenderer (typeof (ProgressBar), typeof (ProgressBarRenderer))]
[assembly: ExportRenderer (typeof (Switch), typeof (SwitchRenderer))]
[assembly: ExportImageSourceHandler (typeof (FileImageSource), typeof (FileImageSourceHandler))]
[assembly: ExportImageSourceHandler (typeof (StreamImageSource), typeof (StreamImagesourceHandler))]
[assembly: ExportImageSourceHandler (typeof (UriImageSource), typeof (ImageLoaderSourceHandler))]
namespace Ooui.Forms
{
@ -26,4 +30,13 @@ namespace Ooui.Forms
{
}
}
[AttributeUsage (AttributeTargets.Assembly, AllowMultiple = true)]
public sealed class ExportImageSourceHandlerAttribute : HandlerAttribute
{
public ExportImageSourceHandlerAttribute (Type handler, Type target)
: base (handler, target)
{
}
}
}

View File

@ -31,7 +31,7 @@ namespace Xamarin.Forms
Registrar.RegisterAll (new[] {
typeof(ExportRendererAttribute),
//typeof(ExportCellAttribute),
//typeof(ExportImageSourceHandlerAttribute),
typeof(ExportImageSourceHandlerAttribute),
});
}

View File

@ -0,0 +1,201 @@
using System;
using System.ComponentModel;
using System.Threading;
using System.Threading.Tasks;
using Xamarin.Forms;
namespace Ooui.Forms.Renderers
{
public class ImageRenderer : ViewRenderer<Xamarin.Forms.Image, Ooui.Image>
{
bool _isDisposed;
protected override void Dispose (bool disposing)
{
if (_isDisposed)
return;
if (disposing) {
}
_isDisposed = true;
base.Dispose (disposing);
}
protected override async void OnElementChanged (ElementChangedEventArgs<Xamarin.Forms.Image> e)
{
if (Control == null) {
var imageView = new Ooui.Image ();
SetNativeControl (imageView);
}
if (e.NewElement != null) {
SetAspect ();
await TrySetImage (e.OldElement);
SetOpacity ();
}
base.OnElementChanged (e);
}
protected override async void OnElementPropertyChanged (object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged (sender, e);
if (e.PropertyName == Xamarin.Forms.Image.SourceProperty.PropertyName)
await TrySetImage ();
else if (e.PropertyName == Xamarin.Forms.Image.IsOpaqueProperty.PropertyName)
SetOpacity ();
else if (e.PropertyName == Xamarin.Forms.Image.AspectProperty.PropertyName)
SetAspect ();
}
void SetAspect ()
{
if (_isDisposed || Element == null || Control == null) {
return;
}
}
protected virtual async Task TrySetImage (Xamarin.Forms.Image previous = null)
{
// By default we'll just catch and log any exceptions thrown by SetImage so they don't bring down
// the application; a custom renderer can override this method and handle exceptions from
// SetImage differently if it wants to
try {
await SetImage (previous).ConfigureAwait (false);
}
catch (Exception ex) {
System.Diagnostics.Debug.WriteLine ("Error loading image: {0}", ex);
}
finally {
((IImageController)Element)?.SetIsLoading (false);
}
}
protected async Task SetImage (Xamarin.Forms.Image oldElement = null)
{
if (_isDisposed || Element == null || Control == null) {
return;
}
var source = Element.Source;
if (oldElement != null) {
var oldSource = oldElement.Source;
if (Equals (oldSource, source))
return;
if (oldSource is FileImageSource && source is FileImageSource && ((FileImageSource)oldSource).File == ((FileImageSource)source).File)
return;
Control.Source = "";
}
IImageSourceHandler handler;
Element.SetIsLoading (true);
if (source != null &&
(handler = Xamarin.Forms.Internals.Registrar.Registered.GetHandler<IImageSourceHandler> (source.GetType ())) != null) {
string uiimage;
try {
uiimage = await handler.LoadImageAsync (source, scale: 1.0f);
}
catch (OperationCanceledException) {
uiimage = null;
}
if (_isDisposed)
return;
var imageView = Control;
if (imageView != null)
imageView.Source = uiimage;
((IVisualElementController)Element).NativeSizeChanged ();
}
else {
Control.Source = "";
}
Element.SetIsLoading (false);
}
void SetOpacity ()
{
if (_isDisposed || Element == null || Control == null) {
return;
}
}
}
public interface IImageSourceHandler : IRegisterable
{
Task<string> LoadImageAsync (ImageSource imagesource, CancellationToken cancelationToken = default (CancellationToken), float scale = 1);
}
public sealed class FileImageSourceHandler : IImageSourceHandler
{
public async Task<string> LoadImageAsync (ImageSource imagesource, CancellationToken cancelationToken = default (CancellationToken), float scale = 1f)
{
string image = null;
var filesource = imagesource as FileImageSource;
var file = filesource?.File;
if (!string.IsNullOrEmpty (file)) {
var name = System.IO.Path.GetFileName (file);
image = "/images/" + name;
if (Ooui.UI.TryGetFileContentAtPath (image, out var f)) {
// Already published
}
else {
await Task.Run (() => Ooui.UI.PublishFile (image, file), cancelationToken);
}
}
return image;
}
}
public sealed class StreamImagesourceHandler : IImageSourceHandler
{
public async Task<string> LoadImageAsync (ImageSource imagesource, CancellationToken cancelationToken = default (CancellationToken), float scale = 1f)
{
string image = null;
var streamsource = imagesource as StreamImageSource;
if (streamsource?.Stream != null) {
using (var streamImage = await ((IStreamImageSource)streamsource).GetStreamAsync (cancelationToken).ConfigureAwait (false)) {
if (streamImage != null) {
var data = new byte[streamImage.Length];
using (var outputStream = new System.IO.MemoryStream (data)) {
await streamImage.CopyToAsync (outputStream, 4096, cancelationToken).ConfigureAwait (false);
}
var hash = Ooui.UI.Hash (data);
var etag = "\"" + hash + "\"";
image = "/images/" + hash;
if (Ooui.UI.TryGetFileContentAtPath (image, out var file) && file.Etag == etag) {
// Already published
}
else {
Ooui.UI.PublishFile (image, data, etag, "image");
}
}
}
}
if (image == null) {
System.Diagnostics.Debug.WriteLine ("Could not load image: {0}", streamsource);
}
return image;
}
}
public sealed class ImageLoaderSourceHandler : IImageSourceHandler
{
public Task<string> LoadImageAsync (ImageSource imagesource, CancellationToken cancelationToken = default (CancellationToken), float scale = 1f)
{
var imageLoader = imagesource as UriImageSource;
return Task.FromResult (imageLoader?.Uri.ToString () ?? "");
}
}
}

View File

@ -73,6 +73,19 @@ namespace Ooui
}
}
}
static bool serverEnabled = true;
public static bool ServerEnabled {
get => serverEnabled;
set {
if (serverEnabled != value) {
serverEnabled = value;
if (serverEnabled)
Restart ();
else
Stop ();
}
}
}
static UI ()
{
@ -91,7 +104,7 @@ namespace Ooui
clientJsEtag = "\"" + Hash (clientJsBytes) + "\"";
}
static string Hash (byte[] bytes)
public static string Hash (byte[] bytes)
{
var sha = sha256;
if (sha == null) {
@ -135,7 +148,47 @@ namespace Ooui
if (contentType == null) {
contentType = GuessContentType (path, filePath);
}
Publish (path, new DataHandler (data, contentType));
var etag = "\"" + Hash (data) + "\"";
Publish (path, new DataHandler (data, etag, contentType));
}
public static void PublishFile (string path, byte[] data, string contentType)
{
var etag = "\"" + Hash (data) + "\"";
Publish (path, new DataHandler (data, etag, contentType));
}
public static void PublishFile (string path, byte[] data, string etag, string contentType)
{
Publish (path, new DataHandler (data, etag, contentType));
}
public static bool TryGetFileContentAtPath (string path, out FileContent file)
{
RequestHandler handler;
lock (publishedPaths) {
if (!publishedPaths.TryGetValue (path, out handler)) {
file = null;
return false;
}
}
if (handler is DataHandler dh) {
file = new FileContent {
Etag = dh.Etag,
Content = dh.Data,
ContentType = dh.ContentType,
};
return true;
}
file = null;
return false;
}
public class FileContent
{
public string ContentType { get; set; }
public string Etag { get; set; }
public byte[] Content { get; set; }
}
public static void PublishJson (string path, Func<object> ctor)
@ -146,7 +199,8 @@ namespace Ooui
public static void PublishJson (string path, object value)
{
var data = JsonHandler.GetData (value);
Publish (path, new DataHandler (data, JsonHandler.ContentType));
var etag = "\"" + Hash (data) + "\"";
Publish (path, new DataHandler (data, etag, JsonHandler.ContentType));
}
public static void PublishCustomResponse (string path, Action<HttpListenerContext, CancellationToken> responder)
@ -178,6 +232,7 @@ namespace Ooui
static void Start ()
{
if (!serverEnabled) return;
if (serverCts != null) return;
serverCts = new CancellationTokenSource ();
var token = serverCts.Token;
@ -343,11 +398,17 @@ namespace Ooui
class DataHandler : RequestHandler
{
readonly byte[] data;
readonly string etag;
readonly string contentType;
public DataHandler (byte[] data, string contentType = null)
public byte[] Data => data;
public string Etag => etag;
public string ContentType => contentType;
public DataHandler (byte[] data, string etag, string contentType = null)
{
this.data = data;
this.etag = etag;
this.contentType = contentType;
}
@ -357,13 +418,20 @@ namespace Ooui
var path = url.LocalPath;
var response = listenerContext.Response;
response.StatusCode = 200;
if (!string.IsNullOrEmpty (contentType))
response.ContentType = contentType;
response.ContentLength64 = data.LongLength;
var inm = listenerContext.Request.Headers.Get ("If-None-Match");
if (!string.IsNullOrEmpty (inm) && inm == etag) {
response.StatusCode = 304;
}
else {
response.StatusCode = 200;
response.AddHeader ("Etag", etag);
if (!string.IsNullOrEmpty (contentType))
response.ContentType = contentType;
response.ContentLength64 = data.LongLength;
using (var s = response.OutputStream) {
s.Write (data, 0, data.Length);
using (var s = response.OutputStream) {
s.Write (data, 0, data.Length);
}
}
response.Close ();
}