Forum Xamarin.Forms

Editing an SVG image programmatically

RicardoSRicardoS ESMember ✭✭✭

So yeah, we added a handful of SVG images to a Forms App we are working on. That is all fine and dandy. The problem is that the images are going to change.

How? Well, let's say that as long as the app is running (which, given its functionality, is almost an entire day) is going to receive a certain amount of data the is going to change the appearance of said SVG images. The data in question is a number and a string containing a colour (in hex).

What do we want to do with this number and hex? Modify the SVG, change the usual black colour into the hex and write the number on top of the image. Sounds simple, yet it's proven to be quite impossible.

Normally, what we do is fill the resources with the same image (png, obviously) in different colours so that, when necessary, changing from one colour to another is as easy as changing the Image's source. BUT, since we do not know the colours beforehand (and also, they are subject to change), we have to manipulate the SVG as the app runs. The integer is no easy feat either.

If someone knows about how to do something like this, we'll be grateful to hear. Please, ask about any info about our project we might have overlooked (packages, frameworks, etc.).

Regards.

P.S.: In ordrer to load the SVG,s, we use SkiaSharp. Any alternative will be welcome. As long as it works in our framework.
Visual Studio for Mac (Community) V. 8.8 (build 2913)
Xamarin.Forms (4.8.0.1687)
.NET Standard 2.0

Tagged:

Best Answers

  • RicardoSRicardoS ESMember ✭✭✭
    Accepted Answer

    Ok, I've got spare minutes to post the solution:

    using System;
    using System.IO;
    using SkiaSharp;
    using SkiaSharp.Views.Forms;
    using Xamarin.Forms;
    using SKSvg = SkiaSharp.Extended.Svg.SKSvg;
    
    namespace PlannerConductores {
        public class Icon : Frame {
            private readonly SKCanvasView _canvasView = new SKCanvasView ();
            public Color TintColor { get; set; }
    
            public static readonly BindableProperty ResourceIdProperty = BindableProperty.Create (
                nameof (ResourceId), typeof (string), typeof (Icon), default (string), propertyChanged: RedrawCanvas);
    
            public string ResourceId {
                get => (string) GetValue (ResourceIdProperty);
                set => SetValue (ResourceIdProperty, value);
            }
    
            public Icon () {
                Padding = new Thickness (0);
                BackgroundColor = Color.Transparent;
                HasShadow = false;
                Content = _canvasView;
                _canvasView.PaintSurface += CanvasViewOnPaintSurface;
            }
    
            private static void RedrawCanvas (BindableObject bindable, object oldvalue, object newvalue) {
                Icon svgIcon = bindable as Icon;
                svgIcon?._canvasView.InvalidateSurface ();
            }
    
            private void CanvasViewOnPaintSurface (object sender, SKPaintSurfaceEventArgs args) {
                SKCanvas canvas = args.Surface.Canvas;
                canvas.Clear ();
    
                if (string.IsNullOrEmpty (ResourceId))
                    return;
    
                SKSvg svg = new SKSvg ();
                using (Stream stream = GetType ().Assembly.GetManifestResourceStream (ResourceId)) {
                    svg = new SKSvg ();
                    svg.Load (stream);
    
                    SKImageInfo info = args.Info;
                    canvas.Translate (info.Width / 2f, info.Height / 2f);
    
                    SKRect bounds = svg.ViewBox;
                    float xRatio = info.Width / bounds.Width;
                    float yRatio = info.Height / bounds.Height;
    
                    float ratio = Math.Min (xRatio, yRatio);
    
                    canvas.Scale (ratio);
                    canvas.Translate (-bounds.MidX, -bounds.MidY);
    
                    canvas.DrawPicture (svg.Picture);
                }
    
                using (var paint = new SKPaint ()) {
                    paint.ColorFilter = SKColorFilter.CreateBlendMode (SKColor.Parse (TintColor.ToHex ()), SKBlendMode.SrcIn);
                    canvas.DrawPicture (svg.Picture);
                    canvas.DrawPicture (svg.Picture, paint);
                }
            }
    
        }
    }
    

    However, this only works when creating the image. Once the code starts you cannot change it, so we are looking for alternatives.

  • RicardoSRicardoS ESMember ✭✭✭
    Accepted Answer

    New modification!

    Instead of calling tint, use this new method to change the colour of your SVG whenever you want:

    using System;
    using System.IO;
    using SkiaSharp;
    using SkiaSharp.Views.Forms;
    using Xamarin.Forms;
    using SKSvg = SkiaSharp.Extended.Svg.SKSvg;
    
    namespace PlannerConductores {
        public class Icon : Frame {
            #region Private Members
            private readonly SKCanvasView _canvasView = new SKCanvasView ();
            public Color TintColor { get; set; }
            #endregion
    
            #region Bindable Properties
            #region ResourceId
            public static readonly BindableProperty ResourceIdProperty = BindableProperty.Create (
                nameof (ResourceId), typeof (string), typeof (Icon), default (string), propertyChanged: RedrawCanvas);
    
            public string ResourceId {
                get => (string) GetValue (ResourceIdProperty);
                set => SetValue (ResourceIdProperty, value);
            }
            #endregion
            #endregion
    
            #region Constructor
            public Icon () {
                Padding = new Thickness (0);
                BackgroundColor = Color.Transparent;
                HasShadow = false;
                Content = _canvasView;
                _canvasView.PaintSurface += CanvasViewOnPaintSurface;
            }
            #endregion
    
            #region Private Methods
            private static void RedrawCanvas (BindableObject bindable, object oldvalue, object newvalue) {
                Icon svgIcon = bindable as Icon;
                svgIcon?._canvasView.InvalidateSurface ();
            }
    
            private void RedrawCanvas () {
                _canvasView.InvalidateSurface ();
            }
    
            private void CanvasViewOnPaintSurface (object sender, SKPaintSurfaceEventArgs args) {
                SKCanvas canvas = args.Surface.Canvas;
                canvas.Clear ();
    
                if (string.IsNullOrEmpty (ResourceId))
                    return;
    
                SKSvg svg = new SKSvg ();
                using (Stream stream = GetType ().Assembly.GetManifestResourceStream (ResourceId)) {
                    svg = new SKSvg ();
                    svg.Load (stream);
    
                    SKImageInfo info = args.Info;
                    canvas.Translate (info.Width / 2f, info.Height / 2f);
    
                    SKRect bounds = svg.ViewBox;
                    float xRatio = info.Width / bounds.Width;
                    float yRatio = info.Height / bounds.Height;
    
                    float ratio = Math.Min (xRatio, yRatio);
    
                    canvas.Scale (ratio);
                    canvas.Translate (-bounds.MidX, -bounds.MidY);
    
                    canvas.DrawPicture (svg.Picture);
                }
    
                using (var paint = new SKPaint ()) {
                    paint.ColorFilter = SKColorFilter.CreateBlendMode (SKColor.Parse (TintColor.ToHex ()), SKBlendMode.SrcIn);
                    canvas.DrawPicture (svg.Picture);
                    canvas.DrawPicture (svg.Picture, paint);
                }
            }
    
            public void ChangeColour (Color tintcolour) {
                TintColor = tintcolour;
                RedrawCanvas ();
            }
            #endregion
        }
    }
    

    Call ChangeColour during any event (a button press, for example) and voilá.

Answers

  • RicardoSRicardoS ESMember ✭✭✭

    I wanted to include the SVG image. Unfortunately, we cannot load it, as a file nor as an image. Sorry.

  • jezhjezh Member, Xamarin Team Xamurai

    Here is some similar thread about this ,you can check it here:

    https://forums.xamarin.com/discussion/144150/how-to-change-the-colour-of-the-svg-image-at-run-time

    The main code is like this:

    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
    <path d="M0 0h24v24H0z" fill="none"/>
    <path fill="heart_filled" opacity="heart_filled" d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z">/>
    </svg>
    
    public class TestIcon : SvgCachedImage
    {
    public TestIcon()
    {
    likeIconColor = Helpers.ColorHelper.GetHexString((Color)Application.Current.Resources["LikeIcon"]);
    
    var dict = new Dictionary<string, string>();
    dict.Add("fill=\"heart_filled\"", "fill=\"" + likeIconColor + "\"");
    dict.Add("opacity=\"heart_filled\"", "opacity=\"0\"");
    
    ReplaceStringMap = dict;
    Source = SvgImageSource.FromFile("favorite_24px.svg");
    }
    }
    
  • RicardoSRicardoS ESMember ✭✭✭
    edited November 2020

    @jezh said:
    Here is some similar thread about this ,you can check it here:

    https://forums.xamarin.com/discussion/144150/how-to-change-the-colour-of-the-svg-image-at-run-time

    The main code is like this:

    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
    <path d="M0 0h24v24H0z" fill="none"/>
    <path fill="heart_filled" opacity="heart_filled" d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z">/>
    </svg>
    
    public class TestIcon : SvgCachedImage
    {
    public TestIcon()
    {
    likeIconColor = Helpers.ColorHelper.GetHexString((Color)Application.Current.Resources["LikeIcon"]);
    
    var dict = new Dictionary<string, string>();
    dict.Add("fill=\"heart_filled\"", "fill=\"" + likeIconColor + "\"");
    dict.Add("opacity=\"heart_filled\"", "opacity=\"0\"");
    
    ReplaceStringMap = dict;
    Source = SvgImageSource.FromFile("favorite_24px.svg");
    }
    }
    

    Tried that one. Apparently the SkiaSharp version of SVG does not contain some of those variables. I will add the Icon Class we are using:

    using System;
    using System.IO;
    using SkiaSharp;
    using SkiaSharp.Views.Forms;
    using Xamarin.Forms;
    using SKSvg = SkiaSharp.Extended.Svg.SKSvg;
    
    namespace PC {
        public class Icon : Frame {
            private readonly SKCanvasView _canvasView = new SKCanvasView ();
    
            public static readonly BindableProperty ResourceIdProperty = BindableProperty.Create (
                nameof (ResourceId), typeof (string), typeof (Icon), default (string), propertyChanged: RedrawCanvas);
    
            public string ResourceId {
                get => (string) GetValue (ResourceIdProperty);
                set => SetValue (ResourceIdProperty, value);
            }
    
            public Icon () {
                Padding = new Thickness (0);
                BackgroundColor = Color.Transparent;
                HasShadow = false;
                Content = _canvasView;
                _canvasView.PaintSurface += CanvasViewOnPaintSurface;
            }
    
            private static void RedrawCanvas (BindableObject bindable, object oldvalue, object newvalue) {
                Icon svgIcon = bindable as Icon;
                svgIcon?._canvasView.InvalidateSurface ();
            }
    
            private void CanvasViewOnPaintSurface (object sender, SKPaintSurfaceEventArgs args) {
                SKCanvas canvas = args.Surface.Canvas;
                canvas.Clear ();
    
                if (string.IsNullOrEmpty (ResourceId))
                    return;
    
                using (Stream stream = GetType ().Assembly.GetManifestResourceStream (ResourceId)) {
                    SKSvg svg = new SKSvg ();
                    svg.Load (stream);
    
                    SKImageInfo info = args.Info;
                    canvas.Translate (info.Width / 2f, info.Height / 2f);
    
                    SKRect bounds = svg.ViewBox;
                    float xRatio = info.Width / bounds.Width;
                    float yRatio = info.Height / bounds.Height;
    
                    float ratio = Math.Min (xRatio, yRatio);
    
                    canvas.Scale (ratio);
                    canvas.Translate (-bounds.MidX, -bounds.MidY);
    
                    canvas.DrawPicture (svg.Picture);
                }
            }
        }
    }
    
  • RicardoSRicardoS ESMember ✭✭✭
    edited November 2020

    And I remembered, I should probably add a XAML snippet, for where I use the SVG code and such:

    <local:Icon x:Name="delPosIm" ResourceId="PlannerConductores.Autob.svg" Margin="5, 2.5, 2.5, 2.5"
                                WidthRequest="150" HeightRequest="87.5" HorizontalOptions="Center" VerticalOptions="Center"/>
    

    local is the local namespace:

    xmlns:local="clr-namespace:PC;assembly=PC"

  • Tracy320Tracy320 Member ✭✭✭
    edited November 2020

    I did a test according to the code you post, but there were always some problems.

    Could you please post a basic demo?

    Besides, I've noticed that the package(SkiaSharp.Extended) you're using hasn't been updated for a long time.

  • RicardoSRicardoS ESMember ✭✭✭

    @Tracy320 said:
    I did a test according to the code you post, but there were always some problems.

    Could you please post a basic demo?

    Besides, I've noticed that the package(SkiaSharp.Extended) you're using hasn't been updated for a long time.

    Yes, we've noticed it's quite old, but hey. If it works...

    Here is the sample we've been using.

    https://github.com/AlexPshul/SvgXF

  • Tracy320Tracy320 Member ✭✭✭

    I am sorry to reply you so long because of my health. Have you solved the problem ?

  • RicardoSRicardoS ESMember ✭✭✭
    edited November 2020

    @Tracy320 said:
    I am sorry to reply you so long because of my health. Have you solved the problem ?

    Yes, indeed. I will post the solution soon!

    Also, I hope you feel alright now.

  • jezhjezh Member, Xamarin Team Xamurai

    Congrats, could you please post your answer so that others who have similar thread will get help from here?

    Thanks in advance. :)

  • RicardoSRicardoS ESMember ✭✭✭

    @jezh said:
    Congrats, could you please post your answer so that others who have similar thread will get help from here?

    Thanks in advance. :)

    Just a moment. I'm juggling like seven projects right now.

  • RicardoSRicardoS ESMember ✭✭✭
    Accepted Answer

    Ok, I've got spare minutes to post the solution:

    using System;
    using System.IO;
    using SkiaSharp;
    using SkiaSharp.Views.Forms;
    using Xamarin.Forms;
    using SKSvg = SkiaSharp.Extended.Svg.SKSvg;
    
    namespace PlannerConductores {
        public class Icon : Frame {
            private readonly SKCanvasView _canvasView = new SKCanvasView ();
            public Color TintColor { get; set; }
    
            public static readonly BindableProperty ResourceIdProperty = BindableProperty.Create (
                nameof (ResourceId), typeof (string), typeof (Icon), default (string), propertyChanged: RedrawCanvas);
    
            public string ResourceId {
                get => (string) GetValue (ResourceIdProperty);
                set => SetValue (ResourceIdProperty, value);
            }
    
            public Icon () {
                Padding = new Thickness (0);
                BackgroundColor = Color.Transparent;
                HasShadow = false;
                Content = _canvasView;
                _canvasView.PaintSurface += CanvasViewOnPaintSurface;
            }
    
            private static void RedrawCanvas (BindableObject bindable, object oldvalue, object newvalue) {
                Icon svgIcon = bindable as Icon;
                svgIcon?._canvasView.InvalidateSurface ();
            }
    
            private void CanvasViewOnPaintSurface (object sender, SKPaintSurfaceEventArgs args) {
                SKCanvas canvas = args.Surface.Canvas;
                canvas.Clear ();
    
                if (string.IsNullOrEmpty (ResourceId))
                    return;
    
                SKSvg svg = new SKSvg ();
                using (Stream stream = GetType ().Assembly.GetManifestResourceStream (ResourceId)) {
                    svg = new SKSvg ();
                    svg.Load (stream);
    
                    SKImageInfo info = args.Info;
                    canvas.Translate (info.Width / 2f, info.Height / 2f);
    
                    SKRect bounds = svg.ViewBox;
                    float xRatio = info.Width / bounds.Width;
                    float yRatio = info.Height / bounds.Height;
    
                    float ratio = Math.Min (xRatio, yRatio);
    
                    canvas.Scale (ratio);
                    canvas.Translate (-bounds.MidX, -bounds.MidY);
    
                    canvas.DrawPicture (svg.Picture);
                }
    
                using (var paint = new SKPaint ()) {
                    paint.ColorFilter = SKColorFilter.CreateBlendMode (SKColor.Parse (TintColor.ToHex ()), SKBlendMode.SrcIn);
                    canvas.DrawPicture (svg.Picture);
                    canvas.DrawPicture (svg.Picture, paint);
                }
            }
    
        }
    }
    

    However, this only works when creating the image. Once the code starts you cannot change it, so we are looking for alternatives.

  • RicardoSRicardoS ESMember ✭✭✭
    Accepted Answer

    New modification!

    Instead of calling tint, use this new method to change the colour of your SVG whenever you want:

    using System;
    using System.IO;
    using SkiaSharp;
    using SkiaSharp.Views.Forms;
    using Xamarin.Forms;
    using SKSvg = SkiaSharp.Extended.Svg.SKSvg;
    
    namespace PlannerConductores {
        public class Icon : Frame {
            #region Private Members
            private readonly SKCanvasView _canvasView = new SKCanvasView ();
            public Color TintColor { get; set; }
            #endregion
    
            #region Bindable Properties
            #region ResourceId
            public static readonly BindableProperty ResourceIdProperty = BindableProperty.Create (
                nameof (ResourceId), typeof (string), typeof (Icon), default (string), propertyChanged: RedrawCanvas);
    
            public string ResourceId {
                get => (string) GetValue (ResourceIdProperty);
                set => SetValue (ResourceIdProperty, value);
            }
            #endregion
            #endregion
    
            #region Constructor
            public Icon () {
                Padding = new Thickness (0);
                BackgroundColor = Color.Transparent;
                HasShadow = false;
                Content = _canvasView;
                _canvasView.PaintSurface += CanvasViewOnPaintSurface;
            }
            #endregion
    
            #region Private Methods
            private static void RedrawCanvas (BindableObject bindable, object oldvalue, object newvalue) {
                Icon svgIcon = bindable as Icon;
                svgIcon?._canvasView.InvalidateSurface ();
            }
    
            private void RedrawCanvas () {
                _canvasView.InvalidateSurface ();
            }
    
            private void CanvasViewOnPaintSurface (object sender, SKPaintSurfaceEventArgs args) {
                SKCanvas canvas = args.Surface.Canvas;
                canvas.Clear ();
    
                if (string.IsNullOrEmpty (ResourceId))
                    return;
    
                SKSvg svg = new SKSvg ();
                using (Stream stream = GetType ().Assembly.GetManifestResourceStream (ResourceId)) {
                    svg = new SKSvg ();
                    svg.Load (stream);
    
                    SKImageInfo info = args.Info;
                    canvas.Translate (info.Width / 2f, info.Height / 2f);
    
                    SKRect bounds = svg.ViewBox;
                    float xRatio = info.Width / bounds.Width;
                    float yRatio = info.Height / bounds.Height;
    
                    float ratio = Math.Min (xRatio, yRatio);
    
                    canvas.Scale (ratio);
                    canvas.Translate (-bounds.MidX, -bounds.MidY);
    
                    canvas.DrawPicture (svg.Picture);
                }
    
                using (var paint = new SKPaint ()) {
                    paint.ColorFilter = SKColorFilter.CreateBlendMode (SKColor.Parse (TintColor.ToHex ()), SKBlendMode.SrcIn);
                    canvas.DrawPicture (svg.Picture);
                    canvas.DrawPicture (svg.Picture, paint);
                }
            }
    
            public void ChangeColour (Color tintcolour) {
                TintColor = tintcolour;
                RedrawCanvas ();
            }
            #endregion
        }
    }
    

    Call ChangeColour during any event (a button press, for example) and voilá.

Sign In or Register to comment.