Christian Droulers

Agile and flexible programmer

Handling SVG is hard

Posted on 2015-04-13

SVG is a pretty neat image format. But it’s not as easy to implement in a web page.

Many different gotchas to watch out for, browser support is not totally there yet. Here are a few things I remember having to deal with.

  1. <img> tags aren’t the best way to inline images
  2. Inlining the SVG XML directly in the page works best, but would increase the payload significantly since I have table of the same images
  3. I wanted to use the least JavaScript possible
  4. It needed to work on all recent browsers.

There were two steps. First, I wanted to use as much of SVG’s capabilities as possible, including styling. Since I wasn’t inline the SVG, I couldn’t style it using CSS in my HTML page’s source. Therefore, I created an IHttpHandler to modify a base SVG file with some extra classes.

Then, on the client side, I implement some JavaScript to add hover functionality.

Here’s the IHttpHandler:

public class SvgHandler : IHttpHandler
{
public void ProcessRequest(HttpContext context)
{
var fileName = Path.GetFileName(context.Request.AppRelativeCurrentExecutionFilePath) ?? string.Empty;
var svgIndex = fileName.LastIndexOf(".svg", StringComparison.InvariantCultureIgnoreCase);
var classIndex = fileName.IndexOf('.');
string[] classNames = new string[0];
string svgPath;
if (svgIndex != classIndex)
{
classNames = fileName.Substring(classIndex, svgIndex - classIndex).Split('.');
svgPath = context.Server.MapPath(fileName.Substring(0, classIndex) + ".svg");
}
else
{
svgPath = context.Server.MapPath(fileName);
}
context.Response.ContentType = "image/svg+xml";
context.Response.Cache.SetExpires(DateTime.UtcNow.AddYears(1));
if (!classNames.Any())
{
// Early return if no changes need to be done.
context.Response.WriteFile(svgPath);
return;
}
var svgDocument = new XmlDocument();
svgDocument.Load(svgPath);
if (svgDocument.DocumentElement != null)
{
var attributes = svgDocument.DocumentElement.Attributes;
var classAttribute = attributes["class"];
if (classAttribute == null)
{
classAttribute = svgDocument.CreateAttribute("class");
attributes.Append(classAttribute);
}
classAttribute.Value = string.Join(" ", classNames);
}
using (var memoryStream = new MemoryStream())
{
svgDocument.Save(memoryStream);
context.Response.BinaryWrite(memoryStream.ToArray());
}
}
public bool IsReusable
{
get { return true; }
}
}
view raw SvgHandler.cs hosted with ❤ by GitHub

The Web.config file must be modified:

<system.webServer>
    <handlers>
        <add name="SvgHandler" verb="*" path="*.svg" type="Namespace.To.SvgHandler"/>
    </handlers>
</system.webServer>

And here is an AngularJS directive for the JavaScript:

angular.module("shared.ui").directive(
"svgHover",
[
() => {
return {
restrict: "A",
link: (scope: ng.IScope, element: ng.IAugmentedJQuery) => {
function getSvg(svg: HTMLObjectElement): SVGElement {
var svgDocument: Document;
try {
svgDocument = svg.getSVGDocument();
} catch (e) {
// IE breaks on the first call. But we can let it go with the setTimeout.
}
// ReSharper disable once Html.TagNotResolved
var svgElement: SVGElement = svgDocument ? <SVGElement>svgDocument.getElementsByTagName("svg")[0] : null;
return svgElement;
};
function svgMouseOver() {
var svgElement = getSvg(<HTMLObjectElement>element.find("object:first")[0]);
// Check if 'hovered' is not already set to make sure it is not set twice
if (svgElement && svgElement.getAttribute("class") && svgElement.getAttribute("class").indexOf("hovered") === -1) {
var currentClass = svgElement.getAttribute("class");
svgElement.setAttribute("class",(currentClass ? currentClass + " " : "") + "hovered");
}
};
function svgMouseLeave() {
var svgElement = getSvg(<HTMLObjectElement>element.find("object:first")[0]);
if (svgElement) {
var currentClass = svgElement.getAttribute("class") || "";
svgElement.setAttribute("class", currentClass.replace("hovered", "").trim());
}
};
element.hover(svgMouseOver, svgMouseLeave);
}
};
}
]);
view raw svgHover.ts hosted with ❤ by GitHub

The IHttpHandler receives requests for all SVG files. It parses the file name and looks for class names in the format svg-file-name.class-names.svg. I could have used the query string, but browsers will usually understand a query string to mean that the file cannot be cached properly.

To display an SVG, it’s a simple matter of using &lt;object&gt;:

<a href="#/some-link" class="btn clickable-svg" svg-hover>
    <object data="/Path/To/SomeSvg.class-name.svg" type="image/svg+xml"></object>
</a>

I had to do a tiny bit of CSS for clickable-svg to work, otherwise, the mouse pointer doesn’t react properly:

a.clickable-svg:after {
    content: "";
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
}

a.clickable-svg {
    position: relative;
}