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.
<img>
tags aren’t the best way to inline images- Inlining the SVG XML directly in the page works best, but would increase the payload significantly since I have table of the same images
- I wanted to use the least JavaScript possible
- 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; } | |
} | |
} |
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); | |
} | |
}; | |
} | |
]); |
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 <object>
:
<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;
}