SVG
SVG copied to clipboard
Consider splitting rendering and processing
In the light of the issue #576 (where the author indicated a .NET standard build might be helpful) we might consider splitting some of the parts from the SVG library. As far as I known part of the rendering (System.Drawing) is the main culprit for the library not being able to be built against .NET standard. From my understanding the SVG library can facilitate 2 things: processing and transforming SVG files (the XML part) and rendering an SVG to a bitmap.
The first part could classify for a .NET standard build. Downside however is that this might result in 2 libraries/packages, but it might help people that want to use the SVG part in .NET standard applications.
Just a random thought, but perhaps something to consider in the future.
This would help a lot with my SkiaSharp port.
This is my current rendering interface used for SkiaSharp: https://github.com/wieslawsoltes/Svg.Skia/blob/4ac1dfb7d58cf9121a472a1d086367b39ba0cf01/src/Svg.Skia/ISvgRenderer.cs#L9 https://github.com/wieslawsoltes/Svg.Skia/blob/master/src/Svg.Skia/SvgRenderer.cs#L11 and a lot of helper methods: https://github.com/wieslawsoltes/Svg.Skia/blob/master/src/Svg.Skia/SvgHelper.cs#L16
So, I understand you are volunteering for this? 😛
So, I understand you are volunteering for this? 😛
I will not be easy to decouple the System.Drawing dependencies from current implementation.
Anyway I will have pretty good idea and some implementation (for Skia) for further discussion when I finish implementing basic support for rendering in the Svg.Skia library.
Sounds good! 👍
See the linked issue, we could evaluate whether we could join forces with the skia drawing repo.
As I wrote on Gitter yesterday, I was thinking about extracting the SVG manipulation part in a separate library too, and make it fully netstandard compliant. My original idea was a completely separate lib, not a split of the this one. But the latter may actually make more sense. I'm just a bit afraid it would be harder and more constrained.
@wieslawsoltes do you think you could achieve the full rendering functionality of vvvv/Svg with your Svg.Skia implementation?
@wieslawsoltes do you think you could achieve the full rendering functionality of vvvv/Svg with your Svg.Skia implementation?
I think we can, a lot of rendering functionality already there.
So creating the new svg parsing/manipulation library seems a good idea for many reasons. If vvvv/Svg will use such libary, should it maintain backward compatibility? I mean, splitting the library without breaking changes could be difficult. What do you think @tebjan?
@tebjan @mrbean-bremen
So my current approach in Svg.Skia
for rendering using SkiaSharp
and document processing from Svg
library is:
Step 1 - load document
var svgDocument = SvgDocument.Open<SvgDocument>(path, null);
if (svgDocument != null)
{
// ...
}
Step 2 - draw using SKCanvas
float width = svgFragment.Width.ToDeviceValue(null, UnitRenderingType.Horizontal, svgFragment);
float height = svgFragment.Height.ToDeviceValue(null, UnitRenderingType.Vertical, svgFragment);
var skSize = new SKSize(width, height);
var cullRect = SKRect.Create(skSize);
using (var renderer = new SKSvgRenderer(skSize))
using (var skPictureRecorder = new SKPictureRecorder())
using (var skCanvas = skPictureRecorder.BeginRecording(cullRect))
{
renderer.DrawFragment(skCanvas, svgFragment);
return skPictureRecorder.EndRecording();
}
Here I am also using SKPictureRecorder
to record SKPicture
. The SKPicture
is nice class that records draw command and you can cheaply redraw the SKPicture
without the costly processing and allocations.
My current custom rendering interface:
public interface ISvgRenderer : IDisposable
{
void DrawFragment(object canvas, SvgFragment svgFragment);
void DrawImage(object canvas, SvgImage svgImage);
void DrawSwitch(object canvas, SvgSwitch svgSwitch);
void DrawSymbol(object canvas, SvgSymbol svgSymbol);
void DrawUse(object canvas, SvgUse svgUse);
void DrawForeignObject(object canvas, SvgForeignObject svgForeignObject);
void DrawCircle(object canvas, SvgCircle svgCircle);
void DrawEllipse(object canvas, SvgEllipse svgEllipse);
void DrawRectangle(object canvas, SvgRectangle svgRectangle);
void DrawMarker(object canvas, SvgMarker svgMarker);
void DrawGlyph(object canvas, SvgGlyph svgGlyph);
void DrawGroup(object canvas, SvgGroup svgGroup);
void DrawLine(object canvas, SvgLine svgLine);
void DrawPath(object canvas, SvgPath svgPath);
void DrawPolyline(object canvas, SvgPolyline svgPolyline);
void DrawPolygon(object canvas, SvgPolygon svgPolygon);
void DrawText(object canvas, SvgText svgText);
void DrawTextPath(object canvas, SvgTextPath svgTextPath);
void DrawTextRef(object canvas, SvgTextRef svgTextRef);
void DrawTextSpan(object canvas, SvgTextSpan svgTextSpan);
}
You can see in all cases I am passing drawing context object canvas
as object
.
In each the draw
method I am casting drawing context canvas
to appropriate SkiaSharp
canvas (it can be Graphics
object when using System.Drawing
).
if (!(canvas is SKCanvas skCanvas))
{
return;
}
Example how draw SvgCircle
element using SkiaSharp
:
float cx = svgCircle.CenterX.ToDeviceValue(null, UnitRenderingType.Horizontal, svgCircle);
float cy = svgCircle.CenterY.ToDeviceValue(null, UnitRenderingType.Vertical, svgCircle);
float radius = svgCircle.Radius.ToDeviceValue(null, UnitRenderingType.Other, svgCircle);
SKRect bounds = SKRect.Create(cx - radius, cy - radius, radius + radius, radius + radius);
SKMatrix matrix = SkiaUtil.GetSKMatrix(svgCircle.Transforms);
skCanvas.Save();
var skPaintOpacity = SkiaUtil.SetOpacity(skCanvas, svgCircle, _disposable);
var skPaintFilter = SkiaUtil.SetFilter(skCanvas, svgCircle, _disposable);
SkiaUtil.SetTransform(skCanvas, matrix);
if (svgCircle.Fill != null)
{
var skPaintFill = SkiaUtil.GetFillSKPaint(svgCircle, _skSize, bounds, _disposable);
skCanvas.DrawCircle(cx, cy, radius, skPaintFill);
}
if (svgCircle.Stroke != null)
{
var skPaintStroke = SkiaUtil.GetStrokeSKPaint(svgCircle, _skSize, bounds, _disposable);
skCanvas.DrawCircle(cx, cy, radius, skPaintStroke);
}
if (skPaintFilter != null)
{
skCanvas.Restore();
}
if (skPaintOpacity != null)
{
skCanvas.Restore();
}
skCanvas.Restore();
When we would have spitted processing of documents from the rendering of them each SvgElement
would implement simple interface with Draw
method:
void Draw(object canvas, ISvgRenderer renderer)
Currently I have to have this ugly hack method as I did not wan;t to change Svg
library yet:
private void Draw(object canvas, SvgElement svgElement)
{
// HACK: Normally 'SvgElement' object itself would call appropriate 'Draw' on current render.
switch (svgElement)
{
case SvgFragment svgFragment:
DrawFragment(canvas, svgFragment);
break;
case SvgImage svgImage:
DrawImage(canvas, svgImage);
break;
case SvgSwitch svgSwitch:
DrawSwitch(canvas, svgSwitch);
break;
case SvgUse svgUse:
DrawUse(canvas, svgUse);
break;
case SvgForeignObject svgForeignObject:
DrawForeignObject(canvas, svgForeignObject);
break;
case SvgCircle svgCircle:
DrawCircle(canvas, svgCircle);
break;
case SvgEllipse svgEllipse:
DrawEllipse(canvas, svgEllipse);
break;
case SvgRectangle svgRectangle:
DrawRectangle(canvas, svgRectangle);
break;
case SvgMarker svgMarker:
DrawMarker(canvas, svgMarker);
break;
case SvgGlyph svgGlyph:
DrawGlyph(canvas, svgGlyph);
break;
case SvgGroup svgGroup:
DrawGroup(canvas, svgGroup);
break;
case SvgLine svgLine:
DrawLine(canvas, svgLine);
break;
case SvgPath svgPath:
DrawPath(canvas, svgPath);
break;
case SvgPolyline svgPolyline:
DrawPolyline(canvas, svgPolyline);
break;
case SvgPolygon svgPolygon:
DrawPolygon(canvas, svgPolygon);
break;
case SvgText svgText:
DrawText(canvas, svgText);
break;
case SvgTextPath svgTextPath:
DrawTextPath(canvas, svgTextPath);
break;
case SvgTextRef svgTextRef:
DrawTextRef(canvas, svgTextRef);
break;
case SvgTextSpan svgTextSpan:
DrawTextSpan(canvas, svgTextSpan);
break;
default:
break;
}
}
Please note that I have not yet implement drawing for some element, so there might be some changes, this is just some ideas from my work with Svg.Skia
.
I might add that I have some experience with abstracting rendering interfaces in my other projects:
One example is my Core2D
drawing application:
https://github.com/wieslawsoltes/Core2D/blob/86dc381798f5fbd03fb24fe0bc1e9f687f45ef80/src/Core2D.Model/Renderer/IShapeRenderer.cs#L12
and the custom renderers implementations:
https://github.com/wieslawsoltes/Core2D/blob/86dc381798f5fbd03fb24fe0bc1e9f687f45ef80/src/Core2D.Renderer.Avalonia/AvaloniaRenderer.cs#L24
https://github.com/wieslawsoltes/Core2D/blob/86dc381798f5fbd03fb24fe0bc1e9f687f45ef80/src/Core2D.Renderer.Dxf/DxfRenderer.cs#L24
https://github.com/wieslawsoltes/Core2D/blob/86dc381798f5fbd03fb24fe0bc1e9f687f45ef80/src/Core2D.Renderer.PdfSharp/PdfSharpRenderer.cs#L19
https://github.com/wieslawsoltes/Core2D/blob/86dc381798f5fbd03fb24fe0bc1e9f687f45ef80/src/Core2D.Renderer.SkiaSharp/SkiaSharpRenderer.cs#L20
https://github.com/wieslawsoltes/Core2D/blob/86dc381798f5fbd03fb24fe0bc1e9f687f45ef80/src/Core2D.Renderer.WinForms/WinFormsRenderer.cs#L21
https://github.com/wieslawsoltes/Core2D/blob/86dc381798f5fbd03fb24fe0bc1e9f687f45ef80/src/Core2D.Renderer.Wpf/WpfRenderer.cs#L25
Also for reference regarding usage of SkiaSharp
, I have created recently pretty advanced drawing application based on SkiaSharp
. Much of inspiration and design of Svg.Skia
is based on this project as the Svg.Skia
was meant to be used in my Draw2D
app.
https://github.com/wieslawsoltes/Draw2D/blob/cc147f6b2980c391a610e27b505c9f664e4ea02d/src/Draw2D.Skia/Renderers/SkiaShapeRenderer.cs#L13
https://github.com/wieslawsoltes/Draw2D/blob/cc147f6b2980c391a610e27b505c9f664e4ea02d/src/Draw2D.Skia/SkiaHelper.cs#L23
Here is current Svg.Skia
implementation with above mentioned interface:
Rendering interface ISvgRenderer
https://github.com/wieslawsoltes/Svg.Skia/blob/334b3f8bbc29136057e7a808ab8cb4becd59c6f5/src/Svg.Skia/Core/ISvgRenderer.cs#L8
SkiaSharp Rendering interface implementation
https://github.com/wieslawsoltes/Svg.Skia/blob/334b3f8bbc29136057e7a808ab8cb4becd59c6f5/src/Svg.Skia/SKSvgRenderer.cs#L12
Utilities for SkiaSharp
https://github.com/wieslawsoltes/Svg.Skia/blob/334b3f8bbc29136057e7a808ab8cb4becd59c6f5/src/Svg.Skia/Skia/SkiaUtil.cs#L16
The SkiaUtil
class is most important part of Svg.Skia
code base. Basically here the Svg
drawing model is translated to SkiaSharp
way of drawing (except few things done in renderer class).
You can see in all cases I am passing drawing context
object canvas
asobject
.In each the
draw
method I am casting drawing contextcanvas
to appropriateSkiaSharp
canvas (it can beGraphics
object when usingSystem.Drawing
).if (!(canvas is SKCanvas skCanvas)) { return; }
i'm wondering why you use object here? you could use ISvgCanvas with different implementations or just leave the parameter out. and pass an ISvgRenderer that manages the canvas.
You can see in all cases I am passing drawing context
object canvas
asobject
. In each thedraw
method I am casting drawing contextcanvas
to appropriateSkiaSharp
canvas (it can beGraphics
object when usingSystem.Drawing
).if (!(canvas is SKCanvas skCanvas)) { return; }
i'm wondering why you use object here? you could use ISvgCanvas with different implementations or just leave the parameter out. and pass an ISvgRenderer that manages the canvas.
@tebjan
The idea is for all drawable Svg element to implement simple interface:
void Draw(object canvas, ISvgRenderer renderer);
example:
public class SvgCircle
{
// ...
public void Draw(object canvas, ISvgRenderer renderer)
{
renderer.DrawCircle(canvas, this);
}
// ...
}
not sure if the canvas object is necessary, why not simply? or am i missing something?
public class SvgCircle
{
// ...
public void Draw(ISvgRenderer renderer)
{
renderer.DrawCircle(this);
}
// ...
}
not sure if the canvas object is necessary, why not simply? or am i missing something?
public class SvgCircle { // ... public void Draw(ISvgRenderer renderer) { renderer.DrawCircle(this); } // ... }
Than you have to initialize ISvgRenderer
implementation with canvas
. In my experience I prefer to pass canvas
as object and just cast to appropriate drawing context.
The canvas
in SkiaSharp
is short-lived, but renderer can persist longer.
before drawing renderer.BeginDraw()
and after renderer.EndDraw()
in those methods each renderer implementation can do what's necessary. then you can do svgDoc.Draw(renderer)
and in Draw
begin and end is called on the interface. this is cleaner design of the abstraction, i think, This also avoids the casting in each method.
usually you have some drawing context
that is used for drawing like SKCanvas
in SkiaSharp
and Graphics
in System.Drawing
that's why I like to pass object canvas
as parameter
not sure how this would work in your example
@tebjan ok I will update my proposal shortly, just need to test 😄
using (var renderer = new SKSvgRenderer(skSize))
using (var skPictureRecorder = new SKPictureRecorder())
using (var skCanvas = skPictureRecorder.BeginRecording(cullRect))
{
renderer.DrawFragment(skCanvas, svgFragment);
return skPictureRecorder.EndRecording();
}
would become
//fields
SkCanvas canvas;
SKPictureRecorder skPictureRecorder;
...
//Begin
skPictureRecorder = new SKPictureRecorder())
skCanvas = skPictureRecorder.BeginRecording(cullRect))
//Draw
skCanvas.DrawSomething()
//End
canvas.Dispose()
skPictureRecorder.Dispose()
....
//and in SvgDocument.Draw(renderer)
try
{
renderer.BeginDraw()
renderer.Draw(this);
}
finally
{
render.EndDraw()
}
@tebjan ok I have refactored renderer interface in Svg.Skia
and it working
public interface ISvgRenderer : IDisposable
{
void DrawFragment(SvgFragment svgFragment);
void DrawImage(SvgImage svgImage);
void DrawSwitch(SvgSwitch svgSwitch);
void DrawSymbol(SvgSymbol svgSymbol);
void DrawUse(SvgUse svgUse);
void DrawForeignObject(SvgForeignObject svgForeignObject);
void DrawCircle(SvgCircle svgCircle);
void DrawEllipse(SvgEllipse svgEllipse);
void DrawRectangle(SvgRectangle svgRectangle);
void DrawMarker(SvgMarker svgMarker);
void DrawGlyph(SvgGlyph svgGlyph);
void DrawGroup(SvgGroup svgGroup);
void DrawLine(SvgLine svgLine);
void DrawPath(SvgPath svgPath);
void DrawPolyline(SvgPolyline svgPolyline);
void DrawPolygon(SvgPolygon svgPolygon);
void DrawText(SvgText svgText);
void DrawTextPath(SvgTextPath svgTextPath);
void DrawTextRef(SvgTextRef svgTextRef);
void DrawTextSpan(SvgTextSpan svgTextSpan);
}
sample usage:
float width = svgFragment.Width.ToDeviceValue(null, UnitRenderingType.Horizontal, svgFragment);
float height = svgFragment.Height.ToDeviceValue(null, UnitRenderingType.Vertical, svgFragment);
var skSize = new SKSize(width, height);
var cullRect = SKRect.Create(skSize);
using (var skPictureRecorder = new SKPictureRecorder())
using (var skCanvas = skPictureRecorder.BeginRecording(cullRect))
using (var renderer = new SKSvgRenderer(skCanvas, skSize))
{
renderer.DrawFragment(svgFragment);
return skPictureRecorder.EndRecording();
}
@wieslawsoltes
When we would have spitted processing of documents from the rendering of them each SvgElement would implement simple interface with Draw method
If we split the library, then the new manipulation-only library shouldn't have any method related to drawing.
I think it's not so bad to have those methods in the ISvgRenderer
interface. But maybe I'm missing something...
Another option could be the extension methods, such as:
public static class SvgCircleExtensions
{
public void Draw(this SvgCircle svgCircle, object canvas, ISvgRenderer renderer)
{ }
}
@wieslawsoltes
When we would have spitted processing of documents from the rendering of them each SvgElement would implement simple interface with Draw method
If we split the library, then the new manipulation-only library shouldn't have any method related to drawing.
I think it's not so bad to have those methods in the
ISvgRenderer
interface. But maybe I'm missing something...Another option could be the extension methods, such as:
public static class SvgCircleExtensions { public void Draw(this SvgCircle svgCircle, object canvas, ISvgRenderer renderer) { } }
The renderer interface depends on backanends and functionality needed to implement complete and correct rendering of supported SVG specification (or parts of spec.).
Each rendering backend (e.g. Gdi, SkiaSharp etc.) have different concepts and may require some additional abstraction in renderer. For example how do you handle transforms, layer, clipping etc..
In my library for now (as its not complete) I have abstract all of internals and above interface in enough, but in my experience it may require some tweaks.
public interface ISvgRenderer : IDisposable { void DrawFragment(SvgFragment svgFragment); void DrawImage(SvgImage svgImage); void DrawSwitch(SvgSwitch svgSwitch); void DrawSymbol(SvgSymbol svgSymbol); void DrawUse(SvgUse svgUse); void DrawForeignObject(SvgForeignObject svgForeignObject); void DrawCircle(SvgCircle svgCircle); void DrawEllipse(SvgEllipse svgEllipse); void DrawRectangle(SvgRectangle svgRectangle); void DrawMarker(SvgMarker svgMarker); void DrawGlyph(SvgGlyph svgGlyph); void DrawGroup(SvgGroup svgGroup); void DrawLine(SvgLine svgLine); void DrawPath(SvgPath svgPath); void DrawPolyline(SvgPolyline svgPolyline); void DrawPolygon(SvgPolygon svgPolygon); void DrawText(SvgText svgText); void DrawTextPath(SvgTextPath svgTextPath); void DrawTextRef(SvgTextRef svgTextRef); void DrawTextSpan(SvgTextSpan svgTextSpan); }
i would even simplify this into:
public interface ISvgRenderer : IDisposable
{
void DrawElement(SvgBaseClass svgElement);
}
in general it's good to keep interfaces as small as possible.
you can then have this switch in the DrawElement
:
public void DrawElement(SvgElement svgElement)
{
switch (svgElement)
{
case SvgFragment svgFragment:
DrawFragment(canvas, svgFragment);
break;
case SvgImage svgImage:
DrawImage(canvas, svgImage);
break;
case SvgSwitch svgSwitch:
DrawSwitch(canvas, svgSwitch);
break;
case SvgUse svgUse:
DrawUse(canvas, svgUse);
break;
case SvgForeignObject svgForeignObject:
DrawForeignObject(canvas, svgForeignObject);
break;
case SvgCircle svgCircle:
DrawCircle(canvas, svgCircle);
break;
case SvgEllipse svgEllipse:
DrawEllipse(canvas, svgEllipse);
break;
case SvgRectangle svgRectangle:
DrawRectangle(canvas, svgRectangle);
break;
case SvgMarker svgMarker:
DrawMarker(canvas, svgMarker);
break;
case SvgGlyph svgGlyph:
DrawGlyph(canvas, svgGlyph);
break;
case SvgGroup svgGroup:
DrawGroup(canvas, svgGroup);
break;
case SvgLine svgLine:
DrawLine(canvas, svgLine);
break;
case SvgPath svgPath:
DrawPath(canvas, svgPath);
break;
case SvgPolyline svgPolyline:
DrawPolyline(canvas, svgPolyline);
break;
case SvgPolygon svgPolygon:
DrawPolygon(canvas, svgPolygon);
break;
case SvgText svgText:
DrawText(canvas, svgText);
break;
case SvgTextPath svgTextPath:
DrawTextPath(canvas, svgTextPath);
break;
case SvgTextRef svgTextRef:
DrawTextRef(canvas, svgTextRef);
break;
case SvgTextSpan svgTextSpan:
DrawTextSpan(canvas, svgTextSpan);
break;
default:
break;
}
}
If we split the library, then the new manipulation-only library shouldn't have any method related to drawing.
the question is then, who is responsible for traversing the element tree correctly and calling the draw methods? the render library shouldn't have to do that, since it is the same for all rendering backends.
the question is then, who is responsible for traversing the element tree correctly and calling the draw methods? the render library shouldn't have to do that, since it is the same for all rendering backends.
In this case, the manipulation library should expose methods to traverse the elements tree. But still it is something not strictly related to rendering.
I mean, the traversal methods shouldn't have any IRenderer
parameter. It's the renderer itself to traverse the tree and do the job.
Agreed. A generic traversal method shall probably live in the manipulation library and be called from the renderer. We could pass it a generic delegate that can be used to do the drawing, or put the recursive drawing code in some rendering base class, that uses the interface for drawing.
I see there has been another attempt to use SkiaSharp for rendering in #291