osu-framework icon indicating copy to clipboard operation
osu-framework copied to clipboard

Support for vector graphics

Open AtomCrafty opened this issue 7 years ago • 7 comments

Opening an issue for this since it has come up repeatedly in discussions. Loading and displaying SVG and similar formats would definitely be a useful addition to the framework.

AtomCrafty avatar Oct 13 '18 22:10 AtomCrafty

I did a bit of research today, trying to find an svg library that can target .NET Standard. So far, the most promising candidate seems to be the appropriately named https://github.com/vvvv/SVG.

It does target .NET Framework, but with vvvv/SVG#326, vvvv/SVG#365 and several manual changes I got it to compile under netstandard 2.0. I also created a simple .NET Core test app, which runs under windows and rasterizes an svg file. Unfortunately I'm apparently too stupid to install dotnet core in my linux vm, so I can't test it there.

Unfortunately, the SVG library is very tightly coupled to the System.Drawing namespace, which is not available under netstandard. The first PR I mentioned uses ZKWeb.System.Drawing to add back the needed functionality, but it has since been discontinued, so I changed it to use System.Drawing.Common instead.

Please note that my fork of the library it its current state is completely hacked together and not really in a presentable state.

AtomCrafty avatar Oct 15 '18 20:10 AtomCrafty

Just a thought: Instead of a library with "external" rendering, how about reading the .svg file (it's XML) and "translating" that to drawables directly? I found a small library which seems to be able to read (but not render, I think?) the .svg format: https://github.com/huysentruitw/svglib

It could be useful for that kind of approach, and supports the .NET Standard without any modifications.

FreezyLemon avatar Dec 07 '18 22:12 FreezyLemon

yes, that would be the only correct path forward

peppy avatar Dec 10 '18 01:12 peppy

Really depends on the cost of rendering those SVGs directly, imho. Pre-rasterizing them in a BufferedContainer-like manner could give huge performance savings if rendering them out-of-the-box is slow.

Tom94 avatar Dec 10 '18 06:12 Tom94

Hi, I recently had the need to load SVG images and I noticed the discussion here and I think I found a way of loading that works well. This is implemented using Svg.Skia, and it is quite lightweight. It is very simple to implement a way to load Svg textures:

using System;
using System.IO;
using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores;
using SixLabors.ImageSharp;
using SkiaSharp;
using Svg.Skia;

namespace osu.Framework.Graphics.Textures;

public class SvgTextureLoaderStore : TextureLoaderStore {

  public SvgTextureLoaderStore(IResourceStore<Byte[]> store) : base(store) {}

  protected override Image<TPixel> ImageFromStream<TPixel>(Stream stream) {
    using SKSvg svg = new();
    svg.Load(stream);
    SKRect bounds = svg.Picture!.CullRect;
    using SKBitmap bitmap = new((Int32)bounds.Width, (Int32)bounds.Height, SKColorType.Rgba8888, SKAlphaType.Premul);
    using SKCanvas canvas = new(bitmap);
    canvas.Clear(SKColors.Transparent);
    canvas.Translate(-bounds.Left, -bounds.Top);
    canvas.DrawPicture(svg.Picture);
    canvas.Flush();
    // The PixelSpan of the bitmap will be copied to the Image, so it's safe to dispose the bitmap.
    return Image.LoadPixelData<TPixel>(bitmap.GetPixelSpan(), bitmap.Width, bitmap.Height);
  }
}

Then use it like a normal image store:

// Load svg store.
[BackgroundDependencyLoader]
private void load() {
  IResourceStore<TextureUpload> svgTextureLoaderStore = new SvgTextureLoaderStore(new NamespacedResourceStore<Byte[]>(this.Resources, @"SvgTexture"));
  this.Textures.AddTextureSource(svgTextureLoaderStore);
}
// Use it.
[BackgroundDependencyLoader]
private void load(TextureStore textureStore) {
  this.InternalChildren.Add(
    new Sprite{Texture = textureStore.Get(@"MyGo.svg")});
  }

I tested it and found that it works well on Windows as well as Android devices. In addition, I would like to ask whether this is being considered to be added to the native support of the framework? That would be great if so, I think this is very important in compressing the size of software, such as fonts and icons, we can use this to store sharp images in very small text files, I would be happy to propose a PR to implement related functions, and discuss some further details.

Frederisk avatar Feb 08 '25 08:02 Frederisk

This is cool to see, but there's a couple of considerations if you'd want to get this into core framework:

  • It's a new library. We'd have to come to a consensus as to whether we want it added.
  • The method you've used means that SVG are rendered at one size ever, rather than re-rasterising on changes to display size.

peppy avatar Feb 08 '25 12:02 peppy

The method you've used means that SVG are rendered at one size ever, rather than re-rasterising on changes to display size.

One of the reasons why there is no implementation here is that I have not thought of a good implementation for the size scaling in the existing base class TextureLoaderStore so that we can simply know the size we need in the Get method, and how to re-rasterize when this size changes in the future. Maybe we need a series of newly designed classes to handle SVG textures separately.

However for changing the size I imagine this would be relatively easy to implement, here is another example I use that allows you to specify a scaling factor to control the rasterization size:

using System;
using System.IO;

using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Textures;

using osuTK;

using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;

using SkiaSharp;

using Svg.Skia;

namespace ZeroV.Game.Utils.ExternalLoader;

internal class SvgLoader : IDisposable
{
  private Boolean disposedValue = false;

  //private SKBitmap bitmap;
  private Image<Rgba32> image;

  private TextureUpload upload;

  public Texture? Texture { get; init; }

  /// <summary>
  /// Load an SVG file and convert it to a Texture.
  /// </summary>
  /// <param name="file">The SVG file to load.</param>
  /// <param name="renderer">The renderer to create the Texture.</param>
  /// <param name="size">The Rasterization size. When it's <see langword="null"/>, the size specified by SVG itself will be used.</param>
  public SvgLoader(FileInfo file, IRenderer renderer, Vector2? size = null)
  {
    using SKSvg svg = new();
    svg.Load(file.FullName);
    //if (svg.Picture is null) {
    //    throw new NullReferenceException(nameof(svg.Picture) + " is null."); ;
    //}
    SKRect bounds = svg.Picture!.CullRect;
    (Single realSizeX, Single realSizeY) = size is null ? (bounds.Width, bounds.Height) : (size.Value.X, size.Value.Y);
    (Single scaleX, Single scaleY) = size is null ? (1, 1) : (size.Value.X / bounds.Width, size.Value.Y / bounds.Height);
    using SKBitmap bitmap = new((Int32)realSizeX, (Int32)realSizeY, SKColorType.Rgba8888, SKAlphaType.Premul);
    using SKCanvas canvas = new(bitmap);
    canvas.Clear(SKColors.Transparent);
    // CullRect may not be at (0, 0)
    canvas.Translate(-bounds.Left, -bounds.Top);
    canvas.Scale(scaleX, scaleY);
    canvas.DrawPicture(svg.Picture);
    canvas.Flush();
    // The PixelSpan of the bitmap will be copied to the Image, so it's safe to dispose the bitmap.
    this.image = SixLabors.ImageSharp.Image.LoadPixelData<Rgba32>(bitmap.GetPixelSpan(), bitmap.Width, bitmap.Height);
    // Image will be disposed after TextureUpload is disposed.
    this.upload = new TextureUpload(this.image);
    this.Texture = renderer.CreateTexture(this.image.Width, this.image.Height);
    // The provided upload will be disposed after the upload is completed.
    this.Texture.SetData(this.upload);
  }

  public void Dispose()
  {
    this.Dispose(disposing: true);
    GC.SuppressFinalize(this);
  }

  protected virtual void Dispose(Boolean disposing)
  {
    if (!this.disposedValue)
    {
      if (disposing)
      {
        this.Texture?.Dispose();
        //this.upload?.Dispose();
        //this.image?.Dispose();
      }
      this.disposedValue = true;
    }
  }
}

We may need to store the SKSvg in the SvgTexture and then re-rasterize it every time the Size changes.

Frederisk avatar Feb 08 '25 13:02 Frederisk