Verschachtelte <use>s in SVG-Symbolen
Verschachteltete <use>
-Elemente innerhalb von SVG-<symbol>
-Elementen werden
von keinem Browser, der mir zur Verfügung steht, wie erwartet angezeigt. Die einzige
Lösung, die ich dafür finden konnte, bestand darin, die Referenzen mit
cheerio
aufzulösen. Es ist zwar umständlich, funktionierte aber in meinem Fall.
Ein Icon-Set für Flaggen
Das Problem tauchte in einem Projekt auf, für das ich ein Icon-Set mit Flaggen
brauchte. Ich beschloss, es mit SVG-Sprites mit <symbol>
-Elementen zu versuchen. Im
Folgenden ist beschrieben, wie man das mit gulp und npm an den Start bringt:
$ mkdir nested-use
$ cd nested-use
$ npm init --force
npm WARN using --force I sure hope you know what you are doing.
Wrote to .../package.json:
...
Als nächstes müssen die Abhängigkeiten installiert werden:
$ npm install --save-dev gulp
...
$ npm install --save-dev gulp-svgmin
...
$ npm install --save-dev gulp-svgstore
...
$ npm install --save-dev flag-icon-css
...
$ npm install --save-dev gulp-rename
...
flag-icon-css ist eine Sammlung von Länderflaggen, die als
einzelne SVGs gespeichert sind. Diese müssen zunächst einmal in einer einzigen,
großen SVG-Datei zusammengeführt werden. Das bewerkstelligt das folgende
gulpfile.js
:
var gulp = require('gulp');
var svgmin = require('gulp-svgmin');
var svgstore = require('gulp-svgstore');
var rename = require('gulp-rename');
gulp.task('default', ['flags']);
gulp.task('flags', function() {
return gulp.src('node_modules/flag-icon-css/flags/4x3/*.svg')
.pipe(svgmin())
.pipe(svgstore())
.pipe(rename('flags.svg'))
.pipe(gulp.dest('.'));
});
Das ist soweit Standard für SVG-Icon-Sets und bedarf nur einer oberflächlichen
Erklärung. Der Gulp-Task flags
ist in Zeile 8 definiert. In Zeile 9 wird
der Stream mit den einzelnen Dateien aus flag-icon-css
befüllt. Die SVGs
werden in Zeile 10 optimiert und dann in Zeile 11 in einer großen SVG-Datei
zusammengeführt. In Zeile 12 wird der Ausgabedateiname auf flags.svg
gesetzt, und die Datei gulp.dest()
auf die Platte geschrieben.
Das Gulpfile wird mit node node_modules/gulp/bin/gulp.js
oder einfach gulp
(falls global installiert) ausgeführt.
Lädt man die SVG-Datei flags.svg
in den Browser, ist man zunächst überrascht,
dass man nichts sieht. Das wird klarer, wenn man die Struktur des generierten
SVGs betrachtet:
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>...</defs>
<symbol id="ad">...</symbol>
<symbol id="ae">...</symbol>
<symbol id="af">...</symbol>
<symbol id="ag">...</symbol>
<symbol id="ai">...</symbol>
...
</svg>
Weil die SVG-Datei minimiert ist, untersucht man die Struktur am besten mit dem DOM-Inspektor der Entwickler-Konsole des Browsers.
Das zusammengeführte SVG enthält ein <defs>
-Element mit einer
Reihe von <clipPath>
s für jedes Icon. Darauf folgen <symbol>
-Elemente,
eins für jede Eingabedatei. Ein <symbol>
ist in etwa das gleiche wie eine
Gruppe (<g>
), nur dass Symbole nicht gerendert werden. Das erklärt, weshalb
der Browser nichts anzeigt.
Damit das passiert, muss eine minimale HTML-Datei erzeugt werden, in der die SVG-Symbole referenziert werden:
<!DOCTYPE html>
<html>
<body>
<svg viewBox="0 0 640 480" width="160" height="120"
style="border: 1px solid #eeeeee;">
<use xlink:href="flags.svg#bg"/>
</svg>
<svg viewBox="0 0 640 480" width="160" height="120"
style="border: 1px solid #eeeeee;">
<use xlink:href="flags.svg#de"/>
</svg>
<svg viewBox="0 0 640 480" width="160" height="120"
style="border: 1px solid #eeeeee;">
<use xlink:href="flags.svg#in"/>
</svg>
</body>
</html>
Was haben wir hier? Es werden drei Flaggen aus der Kollektion angezeigt, die
bulgarische, die deutsche und die indische. Für jede Flagge wird ein
Inline-<svg>
-Element mit etwas Alibi-CSS und einem einzigen Kindelement,
einem <use>
geschrieben. Das <use>
-Element referenziert die Icons mit den
IDs bg
, de
, und in
aus der externen Datei flags.svg
, die wir gerade
erzeugt haben.
Im Browser sieht man jetzt, dass dies für die bulgarische und die deutsche Flagge einwandfrei funktioniert. Nur die indische Flagge wird lediglich als schwarzes Rechteck dargestellt.
Hinweis: Google Chrome zeigt hier überhaupt keine Grafiken an, wenn die
HTML-Datei über einen file:///
-URI geladen wurde. Am besten startet man
daher einen Ad-Hoc-Web-Server und betrachtet die Datei dort.
Struktur der fehlerhaften SVG-Dateien
Das Problem mit der Grafik der indischen Flagge (und einer Reihe anderer
Flaggen) ist, dass sie selber <use>
-Elemente enthält:
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
height="480" width="640" version="1">
<path fill="#f93" d="M0 0h640v160H0z"/>
<path fill="#fff" d="M0 160h640v160H0z"/>
<path fill="#128807" d="M0 320h640v160H0z"/>
<g transform="matrix(3.2 0 0 3.2 320 240)">
<circle r="20" fill="#008"/>
<circle r="17.5" fill="#fff"/>
<circle r="3.5" fill="#008"/>
<g id="d">
<g id="c">
<g id="b">
<g id="a" fill="#008">
<circle r=".875" transform="rotate(7.5 -8.75 133.5)"/>
<path d="M0 17.5L.6 7 0 2l-.6 5L0 17.5z"/>
</g>
<use height="100%" width="100%" xlink:href="#a" transform="rotate(15)"/>
</g>
<use height="100%" width="100%" xlink:href="#b" transform="rotate(30)"/>
</g>
<use height="100%" width="100%" xlink:href="#c" transform="rotate(60)"/>
</g>
<use height="100%" width="100%" xlink:href="#d" transform="rotate(120)"/>
<use height="100%" width="100%" xlink:href="#d" transform="rotate(-120)"/>
</g>
</svg>
Hier hat jemand einen sehr cleveren Weg gefunden, um die auf der indischen
Flagge abgebildete Ashoka Chakra mit Hilfe von <use>
-Elementen
zusammenzupuzzlen, offensichtlich zu clever für alle Browser, die ich testen
konnte, nämlich Firefox 47, Chrome 51 and Safari 9.1.1, alle auf Mac OS X.
Auflösen der <use>
-Elemente
Ein <use>
-Element in SVG ist einfach ein Platzhalter für das Element, das
es referenziert. Es sollte daher möglich sein, dass Problem zu lösen, indem
die <use>
s aufgelöst, also durch das Element, das sie referenzieren,
ersetzt werden.
Wir verwenden bereits gulp-svgmin für die Optimierung der SVG-Grafiken mit
SVGO. Allerdings
hat SVGO kein Plug-In für das Auflösen von <use>
-Elementen, was auch
verständlich ist, weil das SVG dadurch nicht optimiert, sondern vielmehr
unnötig aufgebläht wird.
Ich habe die Transformierung daher mit
cheerio
selbst geschrieben. Zunächst muss aber cheerio
als Dev-Abhängigkeit
installiert werden:
$ npm install --save-dev gulp-cheerio
Das gulpfile.js
muss wie folgt geändert werden:
const gulp = require('gulp');
const svgmin = require('gulp-svgmin');
const svgstore = require('gulp-svgstore');
const rename = require('gulp-rename');
const cheerio = require('gulp-cheerio');
gulp.task('default', ['flags']);
gulp.task('flags', function() {
function inlineUse($) {
$('use').each(function(i, elem) {
var ref = $(this).attr('xlink:href');
if (ref !== undefined && $(ref) !== null) {
var copy = $(ref).clone(),
attributes = $(this).attr();
copy.attr('id', null);
for (var attr in attributes) {
if (attributes.hasOwnProperty(attr)
&& attr !== 'xlink:href') {
var value = attributes[attr];
if (attr === 'transform'
&& copy.attr(attr) !== undefined) {
value = copy.attr(attr) + ' ' + value;
}
copy.attr(attr, value);
}
}
$(this).replaceWith(copy);
} else {
$(this).remove();
}
})
}
return gulp.src('node_modules/flag-icon-css/flags/4x3/*.svg')
.pipe(svgmin())
.pipe(cheerio({
run: inlineUse,
parserOptions: { xmlMode: true }
}))
.pipe(svgstore())
.pipe(rename('flags.svg'))
.pipe(gulp.dest('.'));
});
Was ist hier neu? Natürlich muss gulp-cheerio
eingebunden (Zeile 5)
und in den Zeilen 38 bis 41, zwischen svgmin()
and svgstore()
aufgerufen werden. Die eigentliche Transformation bewerkstelligt die
Funktion inlineUse()
in Zeile 10.
Die Funktion iteriert über alle <use>
-Elemente (Zeile 11). Für alle diese
Elemente, die ein Attribut xref:href
haben, das auf ein existierendes Element
verweist (siehe Zeile 13), wird eine (tiefe) Kopie erzeugt. Hier kann man
optimieren und stattdessen eine flache Kopie erzeugen, die ausreichend ist.
Weil das referenzierte Element eventuell mehrfach dupliziert wird, muss ein
eventuell vorhandenes id
-Attribut gelöscht werden (Zeile 17). Hier droht
eventuell Ungemach, falls die ID anderswo verwendet wird, aber das ist
relativ unwahrscheinlich.
In der Schleife, die in Zeile 18 beginnt, werden alle Attribute des
<use>
-Elements, bis auf das Attribute xlink:href
in den Klon des
referenzierten Elements kopiert. Nur das Attribut transform
wird besonders
behandelt, und mit einem eventuellen transform
-Attribut des Klons
verkettet.
Schließlich wird das <use>
-Element in Zeile 29 durch die modifizierte Kopie
ersetzt, allerdings nicht, wenn es ungültig war. In diesem Falle wird es
stattdessen einfach aus dem DOM entfernt (Zeile 31). Das ist sinnvoll, weil
es so aussieht, dass die bloße Anwesenheit eines <use>
schon die Anzeige
durch den Browser verhindert.
Wird das Gulpfile erneut mit node node_modules/gulp/bin/gulp.js
ausgeführt,
sollte der Browser jetzt nach einem Reload die indische Flagge in voller
Schönheit und mit Ashoka Chakra darstellen.
Fazit
Ich bin wahrlich kein SVG-Experte, und es ist gut möglich, dass es eine viel einfachere Lösung für das Problem gibt. Ich bin auch nicht sicher, ob das von mir beobachtete Browserverhalten ein Defekt ist (der eventuell bald behoben wird) oder einen guten Grund hat. Wer mehr darüber weiß als ich, ist herzlich eingeladen, sein Wissen in einem Kommentar zu teilen.
blog comments powered by Disqus