CSS4J
Embed SVG

Embedding SVG in HTML documents

Overview

SVG images can be used as stand-alone files, but the most efficient way to include them in an HTML document is by embedding them as <svg> subtrees. However, this may cause unexpected problems due to two factors:

  • Element IDs that were unique in the SVG image and the main document may collide, causing the HTML to become non-conformant due to the presence of two (or more) elements with the same ID.
  • The way that STYLE elements are handled in HTML documents may cause further collisions (see below).

The STYLE elements contained inside the hierarchy of the SVG elements are used for the computation of the styles of the whole document. That is, the rules located in a style sheet that was written for a specific SVG image are accounted like if the STYLE was located at the HEAD element (but applying the CSS precedence rules).

Due to that, there could be a collision between IDs and class names in the CSS from the SVG image(s) and the main document, or among the embedded SVG images.

Using the visitor pattern to replace identifiers

Fortunately, the css4j library provides the necessary tools to visit the CSS rules and make the necessary checks and modifications to the relevant selectors. You can use your favourite DOM implementation to check and modify duplicate id attributes (if you use css4j's native DOM, a NodeIterator could be a good choice), but the changes must be consistent with what you apply on the selectors.

For example, the AttributeConditionVisitor class can be of help for the task. Let us subclass it to provide our own logic:

package com.example;

import io.sf.carte.doc.style.css.nsac.AttributeCondition;
import io.sf.carte.doc.style.css.nsac.Condition.ConditionType;
import io.sf.carte.doc.style.css.parser.AttributeConditionVisitor;

class DupeConditionVisitor extends AttributeConditionVisitor {

    @Override
    public void visit(AttributeCondition condition) {
        ConditionType type = condition.getConditionType();
        if (type == ConditionType.ID || type == ConditionType.CLASS) {
            String currentName = condition.getValue();
            if (isBadName(currentName)) {
                String newName = getNewName(currentName);
                setConditionValue(condition, newName);
            }
        }
    }

    private boolean isBadName(String name) {
        // Your logic comes here
        ...
    }

    private String getNewName(String oldName) {
        // Your logic comes here
        ...
    }

}

As you can see, for each visit it is checked whether the selector condition is a ID or class, and in that case checks for the name (for which you need to provide your own logic).

Your visitor based on the above code is now prepared to perform the task, but first you need to find the right CSS rules to apply it, for which we will use another visitor. You could use the AttrStyleRuleVisitor convenience class for this. Let's see an excerpt of what it does, adapted from its source:

package io.sf.carte.doc.style.css.util;

import io.sf.carte.doc.style.css.CSSStyleRule;
import io.sf.carte.doc.style.css.nsac.SelectorList;
import io.sf.carte.doc.style.css.om.AbstractCSSStyleSheet;
import io.sf.carte.doc.style.css.parser.AttributeConditionVisitor;
import io.sf.carte.util.Visitor;

public class AttrStyleRuleVisitor implements Visitor<CSSStyleRule> {

    private final AttributeConditionVisitor visitor;

    public AttrStyleRuleVisitor(AttributeConditionVisitor visitor) {
        super();
        this.visitor = visitor;
    }

    @Override
    public void visit(CSSStyleRule rule) {
        SelectorList selist = rule.getSelectorList();
        visitor.visit(selist);
        rule.setSelectorList(selist); // Refresh serialization
    }

}

As you see, it just accepts the style rule, obtains the selector list and applies the condition visitor to it.

Then you can visit the style sheets by using any of the DOM backends that the library provides. You could gather identifier information from the main document first, then check with the SVG files that you want to embed. Or you may prefer to apply a systematic approach with the identifiers declared in the SVG files.

Here is an example that visits the sheets contained by an element, it uses css4j's native DOM (adapted from Carte's DocumentStore):

DOMElement svg = ... [the SVG element containing the image]

DupeConditionVisitor dupeVisitor = new DupeConditionVisitor();
Visitor<CSSStyleRule> ruleVisitor = new AttrStyleRuleVisitor(dupeVisitor);
Iterator<DOMElement> styleIt = svg.getElementsByTagNameNS("*", "style").iterator();
while (styleIt.hasNext()) {
    DOMElement style = styleIt.next();
    AbstractCSSStyleSheet sheet = (AbstractCSSStyleSheet) ((LinkStyle<?>) style).getSheet();
    if (sheet != null) {
        sheet.acceptStyleRuleVisitor(ruleVisitor);
        style.normalize(); // Write the result to the inner text node
    }
}

And now you only need to serialize the resulting document. (To serialize the native DOM, look at the powerful DOMWriter or just call toString()).

Examples of embedded SVG

You may want to look at the CSS4J benchmark pages for examples of pages where SVG images were embedded in the way described by this document.