Render animated SVG to GIF, AVI or APNG

This example Delphi application demonstrates how to render an animated SVG to a GIF, AVI or animated PNG.

The application makes use of the following libraries:

For the SVG it uses the SVG control package or the demo version of the package.

For the animated PNG, the imaginglib by Marek Mauder

For the AVI, CreateAviFromBitmap by Francois Piette

For GIF, no external library is needed because it is part of the Delphi VCL.

The source code of the application can be found on delphi-svg-control-examples.

The SVG used in this example is “Steam engine slide-valve cylinder animation“.

Application layout

On the top left of the application form there is a TValueListEditor control where you can set the SVG input file, width en height and some other settings.

On the bottom left there is a TTabControl with another TValueListEditor where you can select the output format and some settings for the output.

Then on the top you can specify the “duration” for the animation in milliseconds and the “frames per second”.

The “duration” is something you have to find out yourself by inspecting the SVG file. SVG animations can be very complex, because parts of the animation can be triggered by events. In simple cases you can figure out the duration by looking at the “dur” attributes in the SVG.

For CSS animations, you look for the “animation-duration” property or the short hand version “animation“, which has several animation properties rolled into one property.

Of course, increasing the “frames per second” will result in a smoother animation but a larger output file.

The center pane of the application will display a preview of each frame while it is recorded. You start the recording with the button “Record”.

At the end of the recording, the application will save the output file to the file specified in the output settings.

Stepping through the SVG animation

For stepping through the SVG animation I have created a class TSVGFrameRenderer. Because we do not have to display the SVG animation in real time, the animation timer is not needed.

The total number of frames is calculated by:

FrameCount := Duration * FPS div 1000;

Where FPS is the “Frames per second”.

And the time in milliseconds between each frame is:

DeltaTime :=  1000 div FPS;

The input for the frame renderer is a “SVG root object”, specified by the interface ISVGRoot. The root object contains the parsed SVG file.

The root object also implements interface ISVGAnimationTimerTarget. This interface can be used to step through the SVG animation, for example:

var
  Target: ISVGAnimationTimerTarget;

...

// Get the animation target interface

if not Supports(SVGRoot, ISVGAnimationTimerTarget, Target) then
  Exit;

...

// Start the animation

Target.DoAnimationTimerStart;

...

// Advance the animation "Delay" milliseconds

Target.DoAnimationAdvanceFrame(Delay);

The frame render will render the SVG to a bitmap on each step.

The full code of the frame renderer can be found here.

Rendering to an animated PNG

For rendering the animated PNG we use the imaginglib by Marek Mauder. This is a library for reading, writing and converting many different image formats.

At the moment we use only one output parameter for the PNG animation: “AnimationLoops”. This specifies how many times the animation will repeat itself. A value of 0 means endless repetition.

procedure TSVGConvTargetApng.Convert(aFrameRenderer: TSVGFrameRenderer);
var
  DataArray: TDynImageDataArray;
  Meta: TMetadata;
  Format: TPNGFileFormat;
  Index: Integer;
begin
  // https://github.com/galfar/imaginglib

  SetLength(DataArray, 0);
  try
    if aFrameRenderer.RenderFirst then
    begin

      // Create an array to store the frame images

      SetLength(DataArray, aFrameRenderer.StepCount);

      try
        repeat

          // Convert the bitmap from the frame rendere to an image
          // and store it in the array

          ConvertBitmapToData(aFrameRenderer.Bitmap, DataArray[aFrameRenderer.Step]);

        until not aFrameRenderer.RenderNext;

      finally
        aFrameRenderer.RenderClose;
      end;
    end;

    // Create a meta object for PNG settings

    Meta := TMetadata.Create;
    try
      // Set "AnimationLoops" in the meta object

      Meta.SetMetaItemForSaving(SMetaAnimationLoops, AnimatedLoops);

      // Set the frame delay on each image

      for Index := 0 to Length(DataArray) - 1 do
        Meta.SetMetaItemForSaving(SMetaFrameDelay, 1000 / aFrameRenderer.FPS, Index);

      // Save the array to file in animated PNG format

      Format := TPNGFileFormat.Create(Meta);
      try
        Format.SaveToFile(FileName, DataArray);
      finally
        Format.Free;
      end;

    finally
      Meta.Free;
    end;

  finally
    FreeImagesInArray(DataArray);
  end;
end;

The resulting animated PNG looks like this (will only animate if your browser supports animated PNG).

The imaginglib doesn’t have components for displaying an animated PNG in Delphi (or Freepascal). Below is a very simplistic way to display the animated PNG based on a timer:

uses
  ...
  Imaging,
  ImagingComponents,
  ImagingNetworkGraphics
  ...

TForm1 = class(TForm)
...
  private
    FFrame: Integer;
    FDynImageDataArray: TDynImageDataArray;
  public
    procedure LoadPng(const aFilename: string);
...

procedure TForm1.FormCreate(Sender: TObject);
begin
  SetLength(FDynImageDataArray, 0);
  FFrame := 0;
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  Finalize(FDynImageDataArray);
end;

procedure TForm1.LoadPng(const aFilename: string);
var
  PngFormat: TPNGFileFormat;
  DelayVar: Variant;
begin
  PngFormat := TPNGFileFormat.Create(nil);
  try
    // Load PNG in image array

    PngFormat.LoadFromFile(OpenDialog1.Filename, FDynImageDataArray);

    // Get delay from global meta

    DelayVar := GlobalMetadata.MetaItems[SMetaFrameDelay];
    if not VarIsNull(DelayVar) then
      Timer1.Interval := DelayVar;
  finally
    PngFormat.Free;
  end;

  // Start timer

  FFrame := 0;
  Timer1.Enabled := True;
end;

procedure TForm1.PaintBox1Paint(Sender: TObject);
var
  Bitmap: TBitmap;
begin
  if FFrame < Length(FDynImageDataArray) then
  begin
    Bitmap := TBitmap.Create;
    try
      ConvertDataToBitmap(FDynImageDataArray[FFrame], Bitmap);
      PaintBox1.Canvas.Draw(0, 0, Bitmap);
    finally
      Bitmap.Free;
    end;
  end;
end;

procedure TForm1.Timer1Timer(Sender: TObject);
begin
  PaintBox1.Repaint;

  if FFrame < Length(FDynImageDataArray) - 1 then
    Inc(FFrame)
  else
    FFrame := 0;
end;

Rendering to GIF

In this example we use the GIF functionality that is part of the Delphi VCL, but you could also use the imaginglib by Marek Mauder for this. I based the example below on the GIF animation demo on Andeas Melander’s website. The Gif also takes a parameter “AnimationLoops” for a finite or endless repetition. Also some other parameters which are explained in the Delphi help or on Andreas website.

procedure TSVGConvTargetGif.Convert(aFrameRenderer: TSVGFrameRenderer);
var
  GIFImage: TGifImage;
  GifFrame: TGIFFrame;
  GCE: TGIFGraphicControlExtension;
  LoopExt: TGIFAppExtNSLoop;
begin
  GIFImage := TGifImage.Create;
  try
    if aFrameRenderer.RenderFirst then
    begin
      try
        repeat
          // Add the bitmap to the gif image list

          GifFrame := GifImage.Add(aFrameRenderer.Bitmap);

          // Loop extension must be the first extension in the first frame

          if GifImage.Images.Count = 1 then
          begin
            LoopExt := TGIFAppExtNSLoop.Create(GifFrame);

            // Number of loops (0 = forever)

            LoopExt.Loops := AnimatedLoops;
          end;

          // Add Graphic Control Extension

          GCE := TGIFGraphicControlExtension.Create(GifFrame);

          // Delay is in hundreds of a second

          GCE.Delay := aFrameRenderer.Delay div 10;
          if Transparent then
          begin
            GCE.Transparent := True;
            GCE.TransparentColorIndex :=  GifFrame.Pixels[0, aFrameRenderer.Height - 1];
            GCE.Disposal := dmBackground;
          end;

        until not aFrameRenderer.RenderNext;

      finally
        aFrameRenderer.RenderClose;
      end;
    end;

    // Optimize Color map...

    if OptimizeColorMap then
      GifImage.OptimizeColorMap;

    // Optimize aGifImage frames...

    if OptimizeOptions <> [] then
      GifImage.Optimize(OptimizeOptions, rmNone, dmNearest, 0);

    if not GifImage.Empty then
      GifImage.SaveToFile(Filename);

  finally
    GifImage.Free;
  end;
end;

The GIF format is older technology. It doesn’t support transparency by alpha channel as the PNG format does. On the other hand, it is widely supported. You can sort of simulate transparency by setting the background colour of the SVG. Choose a colour that is not present in the SVG itself. Then set the output parameter “Transparent” to True. The resulting GIF looks like this:

Rendering to AVI

For rendering to AVI, we use the library CreateAviFromBitmap by Francois Piette. The only output setting for the AVI is a compression setting: “None” or “XVID”. “XVID” will only work if you have installed the necessary codec, as explained by Francois on his website.

The code for rendering to AVI is very simple:

rocedure TSVGConvTargetAvi.Convert(aFrameRenderer: TSVGFrameRenderer);
var
  Avi: TAviFromBitmaps;
  CompressionTag: FOURCC;
begin
  // http://francois-piette.blogspot.com/2013/07/creating-avi-file-from-bitmaps-using.html

  // Compression will only work if you have installed the necessary codecs
  // XVID compressor, download the setup from http://www.xvid.org

  CompressionTag := MKFOURCC('D', 'I', 'B', ' ');

  case Compression of
    acXVID: CompressionTag := MKFOURCC('x', 'v', 'i', 'd');
  end;


  Avi := TAviFromBitmaps.CreateAviFile(
    nil,
    Filename,
    CompressionTag,
    Cardinal(aFrameRenderer.FPS),
    1);
  try

    if aFrameRenderer.RenderFirst then
    begin
      try
        repeat
          // Add the bitmap to the avi

          Avi.AppendNewFrame(aFrameRenderer.Bitmap.Handle);

        until not aFrameRenderer.RenderNext;

      finally
        aFrameRenderer.RenderClose;
      end;
    end;

  finally
    Avi.Free;
  end;
end;

Rendering an animated SVG to PNG, GIF or AVI and then using these in your application has the advantage that these later formats do not use as much computing power as an animated might SVG might need.

Simple SVG animations are usually not a problem, but In SVG you can animate almost every attribute, also for example attributes on filters. Rendering SVG filters can be very expensive in terms of computing power, so it might not be possible to render frames fast enough for a smooth animation.

Also you might be less depended on specific fonts or other resources that are needed on the host computer for rendering the SVG.

On the other hand, the SVG file is much smaller. In this example the PNG file is 602KB, the AVI (uncompressed) is 17583KB and the GIF is 492KB. The SVG file is only 13KB.