Processing of SVG Files on Build in .NET

Publication date: 2023-09-17

For some of my projects, I need bitmap images (PNG, to be precise) to be included in the final build. I often prefer to store these images in the form of SVG files, because they are easier to edit.

Earlier, I've been working on MSBuild tooling to process SVGs on build, but eventually I've moved on to a more simple way (and since it was pretty hard to lift that tooling onto modern .NET runtime).

My new way relies on Svg.Skia, more precisely on Svg.Skia.Converter that gets published from the same repository as a .NET tool.

The plan is:

  1. Declare your SVG files as MSBuild items (e.g. <Svg Include="my-file.svg" />).
  2. Install the Svg.Skia.Converter as a .NET tool into your solution, commit the .config/dotnet-tools.json file (for the others to install it using the manifest).
  3. Add a build step that will call dotnet tool restore during the build.
  4. Add a built step that will call dotnet tool run Svg.Skia.Converter --inputFiles @(Svg) --outputDirectory $(TheOutputPathYouWant) during the build.

So, let's begin. First of all, go to the solution directory and execute the following command:

$ dotnet new tool-manifest

This command will generate a .config/dotnet-tools.json file, and will enable you to install local .NET tools (to not have to deal with a global environment).

Then, install the Svg.Skia.Converter tool:

$ dotnet tool install Svg.Skia.Converter

Note it gets installed locally (no --global flag), and should not mess with any other tools or projects you are working on.

Now, let's go write some MSBuild. Create a separate file Svg.props somewhere in your project or solution folder:

<Project>
    <Target Name="DotNetToolRestore"
            BeforeTargets="ProcessSvgFiles"
            Inputs="..\.config\dotnet-tools.json"
            Outputs="$(IntermediateOutputPath)\dotnet-tool-restore.timestamp">
        <Exec Command="dotnet tool restore" WorkingDirectory=".." />
        <Touch Files="$(IntermediateOutputPath)\dotnet-tool-restore.timestamp" AlwaysCreate="true" />
    </Target>

    <Target Name="ProcessSvgFiles" BeforeTargets="Build" DependsOnTargets="DotNetToolRestore"
            Inputs="@(Svg)"
            Outputs="@(Svg->'$(OutputPath)\Resources\%(FileName).png')">
        <Exec Command="dotnet tool run Svg.Skia.Converter --inputFiles @(Svg) --outputDirectory $(OutputPath)\Resources" />
    </Target>
</Project>

This file declares two MSBuild targets.

  1. DotNetToolRestore gets called first, and will just invoke dotnet tool restore in a working directory of .. (the paths are relative to the parent directory of the file containing the target). It will also touch a file obj/dotnet-tool-restore.timestamp for the purpose of caching (to not call the same build step again if there were no changes in the dotnet-tools.json).
  2. ProcessSvgFiles gets called before the Build target, and will invoke dotnet tool run Svg.Skia.Converter to convert all the SVG files into PNGs. It has properly (I hope!) declared the inputs and outputs of this target, so that MSBuild will be able to cache the results of this target and not call it again if there were no changes in the SVG files. The resulting files will go into $(OutputPath)/Resources.

And the final step: in your project file, add the Svg items and don't forget to import Svg.props:

<Project Sdk="Microsoft.NET.Sdk">
    <ItemGroup>
        <Svg Include="submarine.svg" />
    </ItemGroup>
    <Import Project="Svg.props" />
</Project>

And voilá, you have a working build step that will convert your SVG files into PNGs with proper caching, and working across all the supported platforms (well, across all the platforms Svg.Skia.Converter supports, at least).

Note: This section is outdated. This was already fixed in the latest version of Svg.Skia.Converter.

There's one current downside of this approach: the Svg.Skia.Converter got published in a way that it requires .NET Core 3.1 to work, and it won't load on newer runtimes. I've sent a PR fixing that, but a new version hasn't been published yet. So, for now, you'll have to install .NET Core 3.1 runtime on your build agents and developer machines for the whole scheme to work. Watch for the corresponding issue in case that's already fixed.

You can see the whole pipeline working in my project O21 (I am specifically linking a particular commit to make the reference future-proof).