Howto:Extend the Canvas SVG module

From FlightGear wiki
Revision as of 13:24, 21 April 2016 by Hooray (talk | contribs) (→‎tag)
Jump to navigation Jump to search
This article is a stub. You can help the wiki by expanding it.

Motivation

Increasingly, modern avionics/MFDs are re-created in FlightGear using a combination of vector graphics and raster images - often, raster images also serve as the background image. However, for the time being, this must be done manually (in code), because the svg parser used by the Canvas system only supports a tiny subset of SVG tags.

Objective

Document and demonstrate how the Canvas/Nasal SVG module can be extended to suport additional SVG features, by mapping SVG/XML markup to the corresponding Canvas primitives, such as mapping the <image> tag to a Canvas.Image to add support for raster images to the SVG parser.

Background

FlightGear's SVG support is relatively limited - i.e. it's just a tiny subset of SVG that is actually supported by the SVG parser currently, which is then mapped to OpenVG primitives (Canvas.Path).

Often times, it is necessary for people to use only a subset of the features provided by their SVG editor (e.g. Inkscape), or simplify the final result before it can be used in FlightGear by the Canvas system.

Depending on what people have in mind, it may actually be helpful to also learn more about Canvas.Path (see api.nas) and OpenVG in general, because that is how the svg.nas module works behind the scenes when it translates SVG markup to OpenVG paths.

And that's also the easiest way to extend the SVG parser so that it supports additional features, such as for example the image tag.

The SVG standard is rather rich, and many features could be very useful in FlightGear, because the could help greatly reduce the amount of time and expertise required to implement certain features (e.g. loading raster images, supporting line tags or animations in holistic fashion etc).

Test Code

Note  The following code is sufficiently self-contained, so that it can be put into a separate Nasal module, but also executed as a standalone snippet using the Nasal Console
var (width,height) = (320,160);
var svg_filename = "/Nasal/img.svg";
var title = 'SVGTest: '~svg_filename;


# create a new window, dimensions are WIDTH x HEIGHT, using the dialog decoration (i.e. titlebar)
var window = canvas.Window.new([width,height],"dialog")
 .set('title',title);

# adding a canvas to the new window and setting up background colors/transparency
var myCanvas = window.createCanvas().set("background", canvas.style.getColor("bg_color"));

# creating the top-level/root group which will contain all other elements/group
var root = myCanvas.createGroup();

# change the background color 
myCanvas.set("background", "#ffaac0");

# creating the top-level/root group which will contain all other elements/group
var root = myCanvas.createGroup();

var svg_symbol = root.createChild('group');
canvas.parsesvg(svg_symbol, svg_filename);

svg_symbol.setTranslation(width/2,height/2);

#svg_symbol.setScale(0.2);
#svg_symbol.setRotation(radians)

Examples

<image> tag

Let's assume, we'd like to support raster images, e.g. SVG markup such as the following (for the sake of simplicity, this is simply saved under $FG_ROOT/Nasal/img.svg for testing purposes):

<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
  <image x="20" y="20" width="80" height="80" xlink:href="Textures/Splash1.png" />
  <image x="120" y="20" width="80" height="80" xlink:href="Textures/Splash2.png" />
  <image x="220" y="20" width="80" height="80" xlink:href="Textures/Splash3.png" />
  <image x="20" y="120" width="80" height="80" xlink:href="Textures/Splash4.png" />
  <image x="120" y="120" width="80" height="80" xlink:href="Textures/Splash5.png" />
</svg>

By default, the svg parser will bail out with a message saying Skipping unknown element image. Thus, we need to open fgdata/Nasal/canvas/svg.nas and navigate to the line where that error is triggered:

else
    {
      printlog("info", "Skipping unknown element '" ~ name ~ "'");
      skip = level;
      return;
    }

So, the next step is prepending a new conditional block that looks specifically for the new tag that we'd like to see supported, e.g.:

else if (name == "image")
    {
        printlog("alert", "image tag encountered (not yet implemented)");
    }

Or as a diff/patch:

diff --git a/Nasal/canvas/svg.nas b/Nasal/canvas/svg.nas
index d7fe570..c1b4a90 100644
--- a/Nasal/canvas/svg.nas
+++ b/Nasal/canvas/svg.nas
@@ -578,6 +578,10 @@ var parsesvg = func(group, path, options = nil)
       append(defs_stack, "defs");
       return;
     }
+    else if (name == "image") 
+    {
+       printlog("alert", "image tag encountered (not yet implemented)");
+    }
     else
     {
       printlog("info", "Skipping unknown element '" ~ name ~ "'");

Whenever the parser now sees an image tag, it will tell us so saying: parsesvg: image tag encountered (not yet implemented)

The next step is actually mapping the SVG markup to the corresponding Nasal/Canvas code.

However, for starters, we will keep things simple and add a fixed image from $FG_ROOT/Textures whenever an image tag is encountered, e.g. a splash screen texture.

So, the corresponding Nasal/Canvas magic to load a raster image from the base package looks basically like this:

# create an image child 
var child = root.createChild("image").setFile(URL);

There is another issue here, in that the svg parser is dealing with a stack of operations, so we cannot directly access the root element of the Canvas, but need to push primitives onto the stack, which are applied later on.

To learn how this works, it makes sense to look at existing mappings between SVG and Canvas primitives, such as mappings to Canvas.Text and Canvas.Path (add relevant code snippets below):

pushElement('group', attr['id']);

We also need to take into account that, as per the SVG spec, an image tag may not only refer to raster images, but also other vector graphics (SVG files), which means that we need to look at the file extension - i.e. only add a raster image primitive (Canvas.Image) if the extension is not .svg:

For now, this isn't too useful, as it will only load a fixed texture from the base package, so we need to change things to actually process the proper filename/URL, and other attributes:

debug.dump( attr );

For filename/URL handling, we can look at other xlink/href handling in svg.nas and copy/adapt that accordingly:

var ref = attr["xlink:href"];
      if( ref == nil or size(ref) < 2 or ref[0] != `#` )
        return printlog("warn", "Invalid or missing href: '" ~ ref ~ '"');

Once we do have a valid filename/URL, we can simply set the filename property of the new Canvas.Image child. Once again, it makes sense to look at other elements, e.g. Canvas.Text, and its way to set the value of a node:

 pushElement('text', id);
 stack[-1].set("text", tspan.text);

So, now we need to look up the corresponding property that sets the filename, we can do that by opening api.nas and looking up the setFile() method in Canvas.Image:

# Set image file to be used
#
# @param file Path to file or canvas (Use canvas://... for canvas, eg.
#             canvas://by-index/texture[0])
setFile: func(file)
{
  me.set("src", file);
},

As we can now see easily, the correct property to set is named src.

Thus, we can now come up with a suitable attempt at mapping the SVG image tag to a Canvas.Image:

var ref = attr["xlink:href"];
      if( ref == nil )
        return printlog("warn", "Invalid or missing href in image tag: '" ~ ref ~ '"');
pushElement('image',  attr['id']);
stack[-1].set("src", ref); # set the filename to the xlink/href value

The next step is processing the x,y,width and height attributes of the image tag - to do that, we can also refer to existing code: