FFImageLoading icon indicating copy to clipboard operation
FFImageLoading copied to clipboard

SVG Image in iOS-TabbedPage blurry

Open Matti-Koopa opened this issue 6 years ago • 8 comments

I'm trying to add a SVG image to a TabbedPage. It comes up very blurry though. If I try to compensate the size for the screen scale, it comes out way too big and get's larger then the TabBar itself. I tried rendering it in a higher resolution and then downscale it using UIImage.Scale but it get's even worse with that.

Here is my minified TabbedRenderer code:

protected override async Task<Tuple<UIImage, UIImage>> GetIcon(Page page)
{
    int height = TabBar.Frame.Height - TabBar.SafeAreaInsets.Bottom;    
    int size = (int)(height / 2d);
    
    var params = ImageService.Instance.LoadEmbeddedResource(((UriImageSource)page.IconImageSource).File)
        .WithCustomDataResolver(new SvgDataResolver(0, size, false));
    var image = await params.AsUIImageAsync();

    return Tuple.Create(image, (UIImage)null);
}

Matti-Koopa avatar Oct 08 '19 13:10 Matti-Koopa

Possible related to #1305.

I had a similar problem when using SvgImageSource (might also be valid for other sources):

The problem is that scale is ignored in the IImageSourceHandler for iOS. This property should be passed when instantiating the UIImage. I did a quick test and registered my own IImageSourceHandler and everything worked:

public class ScaleAwareImageSourceHandler : IImageSourceHandler
{
	public ScaleAwareImageSourceHandler()
	{
		WrappedImageSourceHandler = new FFImageLoadingImageSourceHandler();
	}

	public IImageSourceHandler WrappedImageSourceHandler { get; }

	public Task<UIImage> LoadImageAsync(ImageSource imageSource, CancellationToken cancellationToken = default, float scale = 1)
	{
		return WrappedImageSourceHandler.LoadImageAsync(imageSource, cancellationToken, scale)
			.ContinueWith(async (task) =>
			{
				UIImage uiImage = await task;

				if (uiImage != null && scale != 1.0)
				{
					return new UIImage(uiImage.CGImage, scale, uiImage.Orientation);
				}

				return uiImage;
			}, cancellationToken)
			.Unwrap();
	}
}

@daniel-luberda I would go for a PR, but I'm not sure where this parameter should be added and passed down to the SvgDataResolver.

Dresel avatar Oct 24 '19 09:10 Dresel

@daniel-luberda Would you be able to look into this?

EvanMulawski avatar Jul 29 '20 16:07 EvanMulawski

can someone has an example on how to write TabbedRenderer for iOS to support svg files? I did not find any examples online.

rtsdemo123 avatar Oct 15 '20 21:10 rtsdemo123

I'm really interested in this too. Because this blur problems also applies to ShellItem Icons and ToolbarIcons. ex with shell item icons : <FlyoutItem Route="tabRoute"> <ShellContent Route="MyPage" Icon="{extensions:SvgImageResource 'assignment-24px.svg', Width=32, Height=32}" /></FlyoutItem>

ex with ToolbarItems : <ToolbarItem IconImageSource="{extensions:SvgImageResource 'assignment-24px.svg', Width=32, Height=32}" />

where extensions:SvgImageResource calls SvgImageSource.FromResource(resource, vectorWidth, vectorHeight) in the end.

@Dresel I saw your post but I can't figure how to use your sample, and give it the the SvgDataResolver. Any ideas ?

BaltoAF avatar Oct 12 '22 08:10 BaltoAF

@Dresel I saw your post but I can't figure how to use your sample, and give it the the SvgDataResolver. Any ideas ?

You need to register it, like FFImageLoading is doing it, something like this.

Dresel avatar Oct 12 '22 11:10 Dresel

@Dresel thak you for your help. I finally succeeded in implementing this. This is what I've done :

1- Add this class based in iOS project, based on Dresel post :

public class ScaleAwareImageSourceHandler : IImageSourceHandler { public ScaleAwareImageSourceHandler() { WrappedImageSourceHandler = new FFImageLoadingImageSourceHandler(); }

    public IImageSourceHandler WrappedImageSourceHandler { get; }

    public Task<UIImage> LoadImageAsync(ImageSource imageSource, CancellationToken cancellationToken = default, float scale = 1)
    {
        return WrappedImageSourceHandler.LoadImageAsync(imageSource, cancellationToken, scale)
            .ContinueWith(async (task) =>
            {
                UIImage uiImage = await task;

                if (uiImage != null && scale != 1.0)
                {
                    return new UIImage(uiImage.CGImage, scale, uiImage.Orientation);
                }

                return uiImage;
            }, cancellationToken)
            .Unwrap();
    }

    public static void Register(Type type, Type renderer)
    {
        var assembly = typeof(Image).GetTypeInfo().Assembly;
        var registrarType = assembly.GetType("Xamarin.Forms.Internals.Registrar") ?? assembly.GetType("Xamarin.Forms.Registrar");
        var registrarProperty = registrarType.GetRuntimeProperty("Registered");

        var registrar = registrarProperty.GetValue(registrarType, null);
        var registerMethod = registrar.GetType().GetRuntimeMethod("Register", new[] { typeof(Type), typeof(Type) });
        registerMethod.Invoke(registrar, new[] { type, renderer });
    }

    /// <summary>
    /// This is a copy of CachedImageRenderer.InitImageSourceHandler(); Replacing the renderer of the SvgImageSource.
    /// </summary>
    public static void InitImageSourceHandler()
    {
        Register(typeof(FileImageSource), typeof(FFImageLoadingImageSourceHandler));
        Register(typeof(StreamImageSource), typeof(FFImageLoadingImageSourceHandler));
        Register(typeof(UriImageSource), typeof(FFImageLoadingImageSourceHandler));
        Register(typeof(EmbeddedResourceImageSource), typeof(FFImageLoadingImageSourceHandler));
        Register(typeof(DataUrlImageSource), typeof(FFImageLoadingImageSourceHandler));
        try
        {
            Assembly assembly = Assembly.Load("FFImageLoading.Svg.Forms");
            if (assembly != null)
            {
                Type type = assembly.GetType("FFImageLoading.Svg.Forms.SvgImageSource");
                if (type != null)
                {
                    Register(type, typeof(ScaleAwareImageSourceHandler));
                }
            }
        }
        catch
        {
        }
    }
}

2- In AppDelegate.cs, use custom implementation of InitImageSourceHandler() :

//CachedImageRenderer.InitImageSourceHandler(); //Use a custom ImageSourceHandler for Svgs. ScaleAwareImageSourceHandler.InitImageSourceHandler();

3- Keep scale to use it later. On Android, scale is always 1. But on iOS it can be found in ScaleHelper.

ScaleAwareImageSourceHandler.InitImageSourceHandler(); .... //ResourceHelper is my helper class. You can use your own. ResourceHelper.Scale = (float)ScaleHelper.Scale;

4 - Use the scale when building the image :

SvgImageSource.FromResource(resourceName, vectorWidth: (int)(width * ResourceHelper.Scale), vectorHeight: (int)(height * ResourceHelper.Scale)

That's all !

BaltoAF avatar Oct 12 '22 13:10 BaltoAF

Thanks for the detailed answer @BaltoAF. I'm trying to implement this in my custom tabbed page renderer in step 4 but I'm not sure how to do this. Could you provide the code for this part? My custom renderer looks like this

public class CustomTabPageRenderer : TabbedRenderer
    {
        protected override async Task<Tuple<UIImage, UIImage>> GetIcon(Page page)
        {
            UIImage imageIcon;

            // Load SVG from file
            UIImage uiImage = await ImageService.Instance.LoadFile(fileImage.File)
                .WithCustomDataResolver(new SvgDataResolver(15, 15, true))
                .AsUIImageAsync();

            imageIcon = uiImage;

            return new Tuple<UIImage, UIImage>(imageIcon, null);
        }
    }

beaver316 avatar Nov 13 '22 13:11 beaver316

Hello ! I'm afraid I can't help you regarding the custom tab renderer, because I don't use one. Instead, I use an AppShell Flyout in Xamarin Forms.

This is what i've done in detail for step 4. It might help you.

4a) Define your tabs in AppShell.xaml. Here is an exemple with two tabs. What is important is the "Icon" value.

<Shell xmlns="http://xamarin.com/schemas/2014/forms" 
       xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
	   xmlns:view="clr-namespace:MyApp.App.Views"
           x:Name="shell"
           x:Class="Balto.App.AppShell">

	<FlyoutItem Route="tabRoute"
                FlyoutDisplayOptions="AsMultipleItems">

        <ShellContent Route="PlanningPage"
                      Title="Planning"
                      Icon="{extensions:SvgImageResource 'event_black_24dp.svg', Width=16, Height=16}"
                      ContentTemplate="{DataTemplate view:PlanningPage}" />
	<ShellContent Route="ExercicesPage"
                      Title="Exercices"
                      Icon="{extensions:SvgImageResource 'assignment-24px.svg', Width=16, Height=16}"
                      ContentTemplate="{DataTemplate view:ExercicesPage}"/>
	</FlyoutItem>
	
</Shell>

4b) To support the Xaml markup extensions:SvgImageResource, I use the following Extension :

/// <summary>
/// Enable to use markup in xaml like this: 
/// <ffimageloadingsvg:SvgCachedImage Source="{extensions:SvgImageResource Images.SeatedMonkey.svg}" />
/// https://docs.microsoft.com/fr-fr/xamarin/xamarin-forms/xaml/markup-extensions/creating
/// </summary>
[ContentProperty("Source")]
public class SvgImageResourceExtension : IMarkupExtension<SvgImageSource>
{
	public string Source { set; get; }

	public int Width { set; get; }

	public int Height { set; get; }

	public SvgImageSource ProvideValue(IServiceProvider serviceProvider)
	{
		if (String.IsNullOrEmpty(Source))
		{
			IXmlLineInfoProvider lineInfoProvider = serviceProvider.GetService(typeof(IXmlLineInfoProvider)) as IXmlLineInfoProvider;
			IXmlLineInfo lineInfo = (lineInfoProvider != null) ? lineInfoProvider.XmlLineInfo : new XmlLineInfo();
			throw new XamlParseException("ImageResourceExtension requires Source property to be set", lineInfo);
		}

		return ResourceHelper.GetSvgImageSource(Source, Width, Height);
	}

	object IMarkupExtension.ProvideValue(IServiceProvider serviceProvider)
	{
		return (this as IMarkupExtension<SvgImageSource>).ProvideValue(serviceProvider);
	}
}

4c) Last : This is a simplified version of the ResourceHelper i'm using. This is where the Scale is used.

public static class ResourceHelper
{
	public static float Scale = 1;
	public static SvgImageSource GetSvgImageSource(string fileName, int width = 0, int height = 0)
	{
		string assemblyName = typeof(ResourceHelper).GetTypeInfo().Assembly.GetName().Name;
		return SvgImageSource.FromResource(assemblyName + ".Resources." + fileName, typeof(ResourceHelper).GetTypeInfo().Assembly, vectorWidth: (int)(width * Scale), vectorHeight: (int)(height * Scale));
	}
}

BaltoAF avatar Nov 15 '22 10:11 BaltoAF