osu-framework
osu-framework copied to clipboard
Support for vector graphics
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.
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.
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.
yes, that would be the only correct path forward
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.
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.
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.
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.