Drawing outlines around SVG elements

The SVG control package offers a number of controls to display SVG graphics. But if you want more control over how SVG graphics are rendered or if you want to post process graphics, you can also use the lower level classes, interfaces and functions of the package.

Two of these interfaces are

ISVGRenderContext

and

ISVGRoot

ISVGRenderContext is a high quality canvas, very much like the Delphi Firemonkey canvas, but it has the advantage that you can use it in Delphi VCL also. It always draws to a bitmap which you have to supply.

It has most of the drawing functions that the Firemonkey canvas has. Functions like DrawCircle, DrawRect, FillRect, FillPath, ApplyFill, ApplyStroke, MultiplyMatrix BeginScene, EndScene and so on.

You can use the render context (RC) for example to add drawings before or after you render an SVG graphic, or you could just use it for high quality drawings, also supporting transparency, something Delphi VCL does not excel in.

ISVGRoot is the root of the tree of SVG elements of the SVG graphic, equivalent to the Document Object Model (DOM)  in an internet browser. This is created after parsing the SVG xml text and allows you to manipulate elements and parameters before actually rendering the graphic. You can also use it to measure element dimensions.

I’ll give an example here how you can use both to render an SVG graphic an then to post process the graphic to draw some outlines around some of the elements in the SVG graphic.

We use the following simple SVG graphic and we will draw outlines around the text, star group an rotated ellipse.

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
  xmlns="http://www.w3.org/2000/svg"
  id="test_svg"
  version="1.1"
  height="320"
  width="480"
  viewBox="0 215 120 80">
  <g 
    id="layer1">
    <text
      id="txt" x="55" y="225" font-family="sans-serif" font-size="8">
      <tspan id="tspan1">A star group</tspan>
      <tspan id="tspan2" x="55" dy="1em">an ellipse</tspan>
      <tspan id="tspan3" x="55" dy="1em">and some text</tspan>
    </text>
    <ellipse
      id="ellipse" transform="rotate(25)" cx="200" cy="215" rx="25" ry="7.5" fill="#00ff00" stroke="#000000" stroke-width="0.75" />
    <g
      id="star_group">
      <path
        id="star1" fill="#ff2a2a" stroke="#000000" stroke-width="0.75"
        d="m 41.010416,262.03718 -4.104352,-0.60791 -3.016279,2.84909 -0.690161,-4.09132 -3.641728,-1.98824 3.677809,-1.92067 0.765567,-4.07789 2.963173,2.90429 4.114874,-0.53204 -1.846468,3.71562 z"
        />
      <path
        id="star2" fill="#ffcc00" stroke="#000000" stroke-width="0.75"
        d="m 25.31441,257.03553 -3.967414,-6.11754 -7.330735,-0.71095 4.676652,-5.60961 -1.579348,-7.09179 6.857743,2.6506 6.354646,-3.67203 -0.438334,7.24778 5.506734,4.82236 -7.128647,1.82876 z"
        />
    </g>
  </g>
</svg>

This SVG graphic looks like this

Now for the code.

In a new Delphi VCL application, using one form with a standard TButton and TImage control, on the OnClick of the button we do the following:

  1. First we will create an SVGRoot object and parse the SVG text
  2. Then we will calculate the size of the entire SVG and set the bitmap size accordingly
  3. Then we will render the SVG to the bitmap
  4. Then we will draw rectangles around the three elements
  5. Last we assign the bitmap to the TImage control

Step 1, create SVGRoot object and parse the SVG text.

uses
  System.UITypes,
  BVE.SVG2SaxParser,
  BVE.SVG2Intf,
  BVE.SVG2Context,
  BVE.SVG2Types,
  BVE.SVG2Elements,
  BVE.SVG2Elements.VCL;

const
  svg_text =
    '<?xml version="1.0" encoding="UTF-8" standalone="no"?>'
  + '<svg xmlns="http://www.w3.org/2000/svg" id="test_svg" version="1.1" width="480" height="320" viewBox="0 215 120 80">'
    + '<g id="layer1">'
      + '<text id="txt" x="55" y="225" font-family="sans-serif" font-size="8">'
        + '<tspan id="tspan1">A star group</tspan>'
        + '<tspan id="tspan2" x="55" dy="1em">an ellipse</tspan>'
        + '<tspan id="tspan3" x="55" dy="1em">and some text</tspan>'
      + '</text>'
      + '<ellipse id="ellipse" transform="rotate(25)" cx="200" cy="215" rx="25" ry="7.5" fill="#00ff00" stroke="#000000" stroke-width="0.75" />'
      + '<g id="star_group">'
       + ' <path id="star1" fill="#ff2a2a" stroke="#000000" stroke-width="0.75"'
          + ' d="m 41.010416,262.03718 -4.104352,-0.60791 -3.016279,2.84909 -0.690161,-4.09132 -3.641728,-1.98824 3.677809,-1.92067 0.765567,-4.07789 2.963173,2.90429 4.114874,-0.53204 -1.846468,3.71562 z" />'
        + '<path id="star2" fill="#ffcc00" stroke="#000000" stroke-width="0.75"'
          + ' d="m 25.31441,257.03553 -3.967414,-6.11754 -7.330735,-0.71095 4.676652,-5.60961 -1.579348,-7.09179 6.857743,2.6506 6.354646,-3.67203 -0.438334,7.24778 5.506734,4.82236 -7.128647,1.82876 z" />'
      + '</g>'
    + '</g>'
  + '</svg>';

procedure TForm1.Button1Click(Sender: TObject);
var
  SVGRoot: ISVGRoot;
  sl: TStringList;
  SVGParser: TSVGSaxParser;
  Bitmap: TBitmap;
  RC: ISVGRenderContext;
  ParentBounds, Bounds: TSVGRect;
begin

  // Step 1
  // Create a VCL SVG Root object

  SVGRoot := TSVGRootVCL.Create;


  // Put the SVG text in a stringlist and parse

  sl := TStringList.Create;
  SVGParser := TSVGSaxParser.Create(nil);
  try
    sl.Text := svg_text; 

    SVGParser.Parse(sl, SVGRoot);

  finally
    SVGParser.Free;
    sl.Free;
  end;


  //..

end;

Step 2, calculate the size of the entire SVG and set the bitmap size accordingly

Next we want to know the dimensions of the SVG graphic. In this case it is not very difficult because the svg element has a with (480) and a height(320) defined. But for this example I’ll show how to calculate the size of an arbitrary SVG graphic.

The ISVGRoot interface has a method “CalcIntrinsicSize”  for calculating the outer dimensions of an SVG graphic.

This method is defined as follows:

 function CalcIntrinsicSize(aRenderContext: ISVGRenderContext; const aParentBounds: TSVGRect): TSVGRect;

So it needs a render context and the bounds of a parent object, this is the outer container. The function returns a rectangle with the dimensions of the SVG graphic.

Why does it need a render context? Because the SVG graphic may contain text and it needs a render context to have access to the font system.

The aParentBounds is used in cases where the SVG size is a percentage of it’s parent or container object.

So to use this we need to create a render context first. Since we don’t have to actually render the SVG yet, we can create a render context based on an arbitrary sized bitmap, so we take a bitmap of size 1 by 1.

procedure TForm1.Button1Click(Sender: TObject);
var
  SVGRoot: ISVGRoot;
  sl: TStringList;
  SVGParser: TSVGSaxParser;
  Bitmap: TBitmap;
  RC: ISVGRenderContext;
  ParentBounds, Bounds: TSVGRect;
begin
  
  // Step 1
  //..

  // Step 2
  // Create a bitmap, we don't know how big it needs to be, so initially
  // we make it the minimum size: 1 by 1

  Bitmap := TBitmap.Create;
  try

    Bitmap.SetSize(1, 1);

    // Now create a render context for the bitmap

    RC := TSVGRenderContextManager.CreateRenderContextBitmap(Bitmap);

    // Define some parent bounds, in case the SVG graphic has dimensions in percentages

    ParentBounds := SVGRect(0, 0, Image1.Width, Image1.Height);

    // Now we can calculate the dimensions of the SVG graphic

    Bounds := SVGRoot.CalcIntrinsicSize(RC, ParentBounds);

    // Resize the bitmap and make it 32bit transparent

    Bitmap.SetSize(Round(Bounds.Width), Round(Bounds.Height));
    Bitmap.PixelFormat := TPixelFormat.pf32bit;
    Bitmap.AlphaFormat := TAlphaFormat.afPremultiplied;

    // Recreate the render context with the newly sized Bitmap!!

    RC := TSVGRenderContextManager.CreateRenderContextBitmap(Bitmap);

    //..

  finally
    Bitmap.Free;
  end;
end;

Step 3, render the SVG to the bitmap

Now we are going to render the SVG to the bitmap using ISVGRoot and ISVGRenderContext.

procedure TForm1.Button1Click(Sender: TObject);
var
  SVGRoot: ISVGRoot;
  sl: TStringList;
  SVGParser: TSVGSaxParser;
  Bitmap: TBitmap;
  RC: ISVGRenderContext;
  ParentBounds, Bounds: TSVGRect;
begin
  // Step 1
  //..

  // Step 2
  //..
  
    // Step 3
    // Draw the SVG on the bitmap

    RC.BeginScene;
    try
      RC.Clear(0); // Clear the bitmap with transparent color
      RC.Matrix := TSVGMatrix.CreateIdentity;

      SVGRenderToRenderContext(
        SVGRoot,
        RC,
        Bounds.Width* 2,
        Bounds.Height,
        [sroEvents],   // Force the creation of an object state tree
        FALSE          // No auto scaling in this case
        );

    finally
      RC.EndScene;
    end;

    // ..

  finally
    Bitmap.Free;
  end;
end;

So just as with drawing on a Firemonkey canvas, we need to enclose any drawing with BeginScene and EndScene commands. We could also modify the matrix of the render context to scale or rotate or translate the SVG graphic.

The global procedure “SVGRenderToRenderContext” is used to draw the SVG contained in “SVGRoot” to the render context “RC”.

This procedure is defined as follows:

procedure SVGRenderToRenderContext(
  aSVGRoot: ISVGRoot;
  aRenderContext: ISVGRenderContext;
  const aWidth, aHeight: TSVGFloat;
  const aRenderOptions: TSVGRenderOptions = [sroFilters, sroClippath];
  const aAutoViewBox: boolean = True;
  const aAspectRatioAlign: TSVGAspectRatioAlign = arXMidYMid;
  const aAspectRatioMeetOrSlice: TSVGAspectRatioMeetOrSlice = arMeet);

The parameter “aRenderOptions” can have a combination of the following settings:

  • sroFilters: SVG filters will be rendered if these are defined in the SVG graphic
  • sroClippath: clippaths will be rendered if these are defined in the SVG graphic
  • sroEvents: mouse pointer events will be enabled for the SVG graphic, for this a Object state tree will be generated

Because filters, clippaths en events need extra, in some cases a lot of resources, they are made optional.

The last option “sroEvents” is interesting if we want to have measurements of individual elements in the SVG graphic. If we want to enable mouse pointer events, we need to know the dimensions of each visible element. The renderer will in that case create a so called “ObjectState” tree , while rendering the SVG graphic.

The “ObjectState” tree will contain a ScreenBBox (bounding box in screen dimensions) of every element visible on the screen. Note that sometimes an element can be drawn multiple times on the screen, if it is referenced by a “use” element. In that case it will be present more than once in the Object State tree.

So we will use the “ObjectState” tree to draw outlines around elements on the rendered SVG.

Step 4, draw rectangles around the three elements and step 5, assign bitmap to TImage control

procedure TForm1.Button1Click(Sender: TObject);
var
  SVGRoot: ISVGRoot;
  sl: TStringList;
  SVGParser: TSVGSaxParser;
  Bitmap: TBitmap;
  RC: ISVGRenderContext;
  ParentBounds, Bounds: TSVGRect;
  StateRoot, State: ISVGObjectState;
  Stroke: TSVGBrush;

  function FindID(aParent: ISVGObjectState; const aID: string): ISVGObjectState;
  var
    Child: ISVGObjectState;
  begin
    // Find first occurence of aID in object state tree

    for Child in aParent.Children do
    begin
      if Child.SVGObject.ID = aID then
      begin
        Result := Child;
        Exit;
      end else
        Result := FindID(Child, aID);
    end;
  end;

begin
  // Step 1
  // ..


  // Step 2
  // ..

    // Step 3
    // Draw the SVG on the bitmap

    RC.BeginScene;
    try
      RC.Clear(0); // Clear the bitmap with transparent color
      RC.Matrix := TSVGMatrix.CreateIdentity;

      SVGRenderToRenderContext(
        SVGRoot,
        RC,
        Bounds.Width* 2,
        Bounds.Height,
        [sroEvents],   // Force the creation of an object state tree
        FALSE          // No auto scaling in this case
        );

      // Step 4
      // Create a brush for stroking the outline

      // Get a reference to the object state tree

      StateRoot := SVGRoot.SVG.ObjectStateRoot;

      // Find the text element on ID and return the screen bounding box

      State := FindID(StateRoot, 'txt');
      if assigned(State) then
      begin
        // Get the screen bounds of the element

        Bounds := State.ScreenBBox;

        // Grow the box a bit

        Bounds.Inflate(5, 5);

        RC.ApplyStroke(TSVGSolidBrush.Create(SVGColorRed), 2.0);

        // Draw a rectangle with rounded corners around the element

        RC.DrawRect(Bounds, 5, 5);
      end;

      // Find the star group element on ID and return the screen bounding box

      State := FindID(StateRoot, 'star_group');
      if assigned(State) then
      begin
        Bounds := State.ScreenBBox;
        Bounds.Inflate(5, 5);

        RC.ApplyStroke(TSVGSolidBrush.Create(SVGColorBlue), 2.0);

        RC.DrawRect(Bounds, 5, 5);
      end;

      // Find the ellipse element on ID and return the screen bounding box

      State := FindID(StateRoot, 'ellipse');
      if assigned(State) then
      begin
        Bounds := State.ScreenBBox;
        Bounds.Inflate(5, 5);

        RC.ApplyStroke(TSVGSolidBrush.Create(SVGColorGreen), 2.0);

        RC.DrawRect(Bounds, 5, 5);
      end;

    finally
      RC.EndScene;
    end;

    // Step 5
    // Assign to Image1

    Image1.Picture.Assign(Bitmap);
  finally
    Bitmap.Free;
  end;
end;

This produces the following output on the VCL form.

Sources can be found on github. To compile the examples, you need the demo or the full version of the SVG control package.

Leave a Comment