Adam Rajnoha

webový kodér, hacker, pekař, táta

Vyřezávání SVG spritu

05.01.2022

V práci používáme jeden velký SVG sprite pro ikony do systému.
Je to pozůstatek z předchozí verze a pro nový codebase máme systém lepší.
Mým dnešním úkolem bylo "vykuchat" ikony ze sprite mapy a uložit je jako samostatné SVG soubory bez použití rozměrů přesných rozměrů a s viewBoxem, tak aby byly respozivní a aby canvas zahrnoval pouze je (z vektorových editorů známo jako Fit To Content). Prolítl jsem původní soubor a zjistil, že je tam 172 ikon. V editoru SVGEdit jsem si nejprve otevřel celý sprite, označil jednotlivou skupinu křivek, zbytek smazal, ořezal plátno podle rozměrů a uložil. Tohle trvalo asi 2 minuty, protože hlavně výběr uzlů křivek je ručně hodně krkolomný a pořád se musí pracovat s celým dokumentem, tzn. proces náchylný k chybám a hlavně nudný. Zabralo by mi to několik hodin.

Potřeboval jsem tedy najít lepší řešení. Otevřel jsem si proto sprite v prohlížeči. Letmo jsem projel strukturu a zjisil jsem, že jednoduché ikonky jsou zapsány jako <path> a složitější jako <g> skupina s různými tvary uvnitř. A že cesty a skupiny rezidují jako přímo podřazené prvky hlavnímu <svg> elementu. No a když už jsem byl v prohlížeči, rozhodl jsem se vyjmout jednotlivé ikony a obalit je do jednotlivých <svg> tagů javascriptem. Ve funkci jsem tedy vzal potomky hlavního svg, dynamicky udělal jednotlivé <svg> elementy a vše naházel do jakéhosi kontajneru "place".


    for (let vector of svg.childNodes) {
        let code = vector.outerHTML;
        let newly = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        newly.setAttribute("xmlns", "http://www.w3.org/2000/svg");
        newly.innerHTML = code;
        place.innerHTML += newly.outerHTML;
    }
            

Potom jsem potřeboval naplnit viewBox, aby to byly validní responzivní SVG soubory. K tomu se nabízí funkce getBBox(), která vrací vždy aktuální "bounding box", i kdyby ikona ještě nebyla vyrendrovaná a oproti getBoundingClientRect() není závislá na viewportu.


    for (let image of nodes) {
        let bbox = image.getBBox();
        let viewBox = [bbox.x, bbox.y, bbox.width, bbox.height].join(" ");
        image.setAttribute("viewBox", viewBox);
    }
            

V tento moment už mám v elementu "place" hezky pod sebou validní svg prvky s danými ikonami, teď je jen dostat do filesystemu. Nejprve jsem chtěl zůstat u javascriptu ale nejsem blázen a už tak je JS zneužívaný horem dolem, takže jsem nápad s postupným vykreslováním do <canvas>u a následným extrahováním blobu a vynucením stažení jako souboru zahodil (zkusil jsem to, Firefox spadnul). V DevTools jsem si teda jen zkopíroval outerHTML mého "place" elementu, zformátoval si jej v textovém editoru tak, aby byla vždy uzavírací </svg> značka na samostatném řádku a z tohoto pěkného souboru jsem nařezal jednotlivé obrázky pomocí csplitu (nejprve jsem zkoušel awk, ale csplit mi příjde srozumitelnější)


    csplit -ffile -b%03d.svg -s --suppress-matched place.html /^\<\/svg\>/ {*}
            

Téměř hotovo. Zatím nejde žádný z obrázků otevřít, protože nemá zavírací </svg> značku. Csplit, ale i awk, ukrajují delimiter z výsledku. Resp. chci, aby to dělali, jinak v mém případě zůstane nešťastně předchozí </svg> značka, následuje <svg> a kód ikony a poté nic. Proto pomocí flagu --suppress-matched aspoň odstraním první </svg> jakožto pozůstatek předchozí ikony, no a pak všem 172 souborům jen přidám správnou zavírací značku. (Rád používám core utility tak, jak jdou myšlenkami po sobě, i když nějaký program zvládne celou operaci, mnohdy je snazší napsat pipe ze tří příkazů, tak jak proces jde v hlavě, než se snažit napsat jeden robustní příkaz)


    echo "</svg>" | tee -a file*.svg
            

Tak, a teď už zbývá jen poslední manuální otrava, pojmenovat ikony podle jejich použití (vykradl bych to podle CSS tříd, ale spojovat pozice je asi ještě na dýl) :)

PS: Samozřejmě, pokud porovnám .tar.gz všech ikon a stejně-komprimovaný původní sprite, dostanu se asi na 130% velikosti kvůli obalovacím <svg> tagům a viewboxu, ale jednak je to zanedbatelný rozdíl při použití v aplikaci, druhak nikdy nezobrazujeme ani 20% ikon zároveň, tudíž přenos to neohrozí a hlavní důvod, proč to celé děláme je fragmentace práce. Doplnit novou ikonu doposud znamenalo opatrně vložit do canvasu, případně ještě canvas zvětšit, najít vhodné místo, povýšit název souboru a zpropagovat změnu do systému. Nyní přidání nebo změna ikony znamená jen nový soubor v gitu. Zároveň je to přpraveno pro modernější způsob servírování, třeba pomocí svg fragmentů. Pokud taky někdo bude chtít tu či onu ikonu použít kdekoliv v jiném projektu, jednoduše si ji najde v souborech.