Zum Hauptinhalt springen

Professionelle Icons

·3938 Wörter

Meine Intention mit diesem Artikel ist es, zu zeigen, wie mit den OpenSource-Werkzeugen Inkscape, ImageMagick und einem Python-Skript professionelle Icons in mehreren Auflösungen erzeugt werden können.

Entwurfsgitter und Icon

Einleitung #

Das Icon einer Anwendung ist ihr Aushängeschild. Egal ob es sich um ein Desktopprogramm, eine Mobil-App oder eine Website handelt, jedes mal wenn man die Anwendung startet, nimmt man es bewusst oder unbewusst wahr. Es ist ein wenig wie der Haupteingang eines Gebäudes. Aber es begleitet einen auch während die Anwendung läuft. Es ist in Task- und Titelleisten zu sehen und stellt eine starke assoziative Verbindung zu der Anwendung her.

Entwickelt man eine eigene Anwendung, ist ein professionelles Icon die Kirsche auf der Torte. Ein gutes Icon wird immer scharf dargestellt. Egal ob es ganz klein, mit 16×16 Pixeln in einem Favoritenmenü, oder ganz groß, mit 288×288 Pixeln für eine Verknüpfung auf einem High-DPI-Desktop, dargestellt wird. Es hat einen guten Farbkontrast, so dass das Motiv gut zu erkennen ist. Und das unabhängig davon ob das Icon auf einem dunklen oder einem hellen Hintergrund erscheint.

In diesem Artikel beschreibe ich ein Verfahren, mit dem man mit OpenSource-Werkzeugen und einem Python-Skript aus einer oder wenigen Vektorgrafiken professionelle Icons erzeugen kann. Dabei gehe ich weniger darauf ein, was bei der grafischen Gestaltung zu beachten ist. Vielmehr gehe ich auf die technischen Aspekte des Icon-Designs ein.

Unschärfen #

Eines der größten Probleme bei Icons ist Unschärfe durch Skalierung. Icons müssen auf dem Desktop oder im Browser in unterschiedlicher Größe dargestellt werden. Verlustfrei skalierbare Vektorgrafiken1 haben sich für Icons aber bisher nicht durchgesetzt. Deshalb sind alle Betriebssysteme und Browser in der Lage, Rastergrafiken2 (Bitmaps) für eine benötigte Auflösung zu skalieren. Das bedeutet, sie können bei Bedarf aus einem vorliegenden Bitmap des Icons ein neues Bitmap erzeugen, welches in der Größe abweicht. Dabei werden die Farbwerte der Pixel durch Interpolationsalgorithmen3 (Dichteinterpolation) verknüpft und neu ermittelt.

Man könnte daher annehmen, das Icon in der höchsten benötigten Auflösung mit allen Details vorzubereiten genügt. Den Rest erledigt das Betriebssystem oder der Browser. Leider treten aber beim Skalieren an Pixelübergängen mit hohen Kontrasten unweigerlich Unschärfen auf. Gerade bei kleinen Auflösungen nimmt man diese dann als unangenehm war. Das Icon wirkt “matschig”.

Die folgende Grafik zeigt ein Bitmap in der Auflösung 16×16 Pixel. Einmal direkt gerastert aus einer Vektorgrafik, und dreimal interpoliert aus Bitmaps mit unterschiedlichen Auflösungen (24×24, 32×32 und 64×64 Pixel). Es wird deutlich, dass die Kantenschärfe bei dem direkt mit 16 Pixeln angelegten Bitmap besser ist, als bei den interpolierten Varianten.

Interpolation bei kleinen Icons
Interpolation bei kleinen Icons

Diesem Effekt wird dadurch entgegengewirkt, dass das Icon in mehreren Auflösungen vorbereitet wird. Es werden Bitmaps in mehreren Auflösungen angelegt. Und zwar so, dass jedes scharf ist. Übliche Bildgrößen sind dabei z. B. 16, 20, 24, 32, 48, 128 und 512 Pixel Kantenlänge. Bei kleineren Auflösungen werden mehr Varianten erzeugt, da die Unschärfen an den Pixelübergängen dort besonders auffallen. Die Unschärfen fallen bei höheren Auflösungen nicht mehr so stark auf. Daher genügen zusätzlich einige wenige hoch aufgelöste Varianten, die bei Bedarf herunterskaliert werden können.

Das Betriebssystem folgt bei der Darstellung dem folgenden Algorithmus:

  1. Suche in den verfügbaren Auflösungen nach einer exakt passenden Variante für die benötigte Bildgröße. Wenn eine exakt passende Variante gefunden wurde, verwende diese unverändert.
  2. Wenn keine passende gefunden wurde, suche nach der kleinsten Variante, die größer ist als die benötigte Bildgröße. Verkleinere diese auf die benötigte Bildgröße. So bleiben möglichst viele Details erhalten. Es entstehen aber Unschärfen.
  3. Gibt es keine größere Variante, nimm die größte verfügbare Variante (welche nun kleiner ist, als die benötigte Bildgröße) und vergrößere diese. Dabei tritt noch mehr Unschärfe auf, da Bilddetails fehlen.

Beispiel: Die benötigte Bildgröße ist 36 Pixel. Verfügbar sind die Auflösungen 16, 24, 32, 48 und 128 Pixel Kantenlänge. Da kein Bild mit 36 Pixel Kantenlänge verfügbar ist, wird das nächst größere Bilde mit 48 Pixeln gewählt. Dieses wird dann auf 36 Pixel herunterskaliert.

Mit fortschreitender Entwicklung der Display-Technik werden Icons in immer höheren Auflösungen benötigt. Auf modernen Smartphone-Displays (> 250dpi) und auf High-DPI-Desktop-Bildschirmen (> 180dpi) werden auch kleine Icons fast immer mit mehr als 32 Pixeln Kantenlänge dargestellt. Wenn ein Icon dafür skaliert werden muss, fällt die entstandene Unschärfe wenig auf. Auf klassischen Desktop-Bildschirmen (96 dpi) werden Icons aber häufig mit Kantenlängen zwischen 16 und 32 Pixeln dargestellt. In diesem Bereich fällt die durch Skalierung entstandene Unschärfe deutlich auf.

Um der beim Skalieren entstandenen Unschärfe entgegenzuwirken, kann ein Schärfefilter eingesetzt werden. Dieser verstärkt den Kontrast an den Kanten. Er kann aber keine verlorenen Details rekonstruieren. Ein Schärfefilter erzeugt bei zu starker Anwendung Artefakte. Bei vorsichtigem Einsatz kann er das Ergebnis jedoch verbessern.

Dateiformat und Auflösungen #

Abhängig davon, auf welcher Plattform ein Icon verwendet werden soll, werden unterschiedliche Anforderungen an Dateiformat und Bildauflösung gestellt.

Das Dateiformat JPEG ist für Icons wenig geeignet, da es durch verlustbehaftete Kompression, bereits ohne zusätzliche Skalierung, Artefakte und Unschärfen zur Folge hat. Dateiformate ohne oder mit verlustloser Kompression (PNG, ICO) sind besser geeignet. Das GIF-Formt kann nur 256 Farben darstellen und bringt hier keine Vorteile gegenüber dem PNG-Format. Sein Vorteil gegenüber anderen üblichen Bitmap-Formaten, nämlich dass es animierte Grafiken enthalten kann, kommt hier bei einem statischen Icon nicht zum Tragen.

Das Icon einer Windows-Anwendung muss i. d. R. als ICO-Datei vorliegen. Diese enthält die gleiche Grafik als Bitmap in mehreren Auflösungen. ICO-Dateien können eine Grafik neben verschiedenen Auflösungen auch in unterschiedlichen Farbtiefen enthalten (Schwarz/Weiß, 16-Farben, 256-Farben, 24-Bit-RGB, 32Bit-RGBA). Das war in der Vergangenheit sinnvoll, da ältere Grafikkarten in einem Grafikmodus mit weniger als 24-Bit-RGB Farbtiefe betrieben wurden. Da aktuelle Computer-Hardware und Smartphones aber ausnahmslos mindestens 24-Bit-RGB-Farbe und damit auch 32-Bit-RGBA-Bitmaps unterstützen, wird auf die Farbtiefe in diesem Artikel nicht weiter eingegangen. Alle PNG- und ICO-Dateien werden in 32-Bit-RGBA erzeugt.

Für eine Mobil-App unterscheiden sich die Anforderungen je nach Plattform (z B. Android4 oder iOS5). Dort wird das Icon als PNG in verschiedenen Auflösungen benötigt.

Für eine Website kann man sich auf das Favicon6 beschränken (ICO oder PNG). Wenn man zusätzlich ein Icon für das Anpinnen der Website auf dem Startbildschirm von Android unterstützen möchte, erstellt man ein Webanwendungsmanifest7,8. Dieses verweist auf Grafikdateien (PNG, ICO, WEBP, u. a.) in mehreren Auflösungen. Für iOS muss man im HTML-Quelltext auf mehrere Varianten eines Apple Touch Icons9 verweisen.

In der folgenden Tabelle sind einige Auflösungen aufgeführt, die aktuell verwendet werden. Da Symbole auf Desktop-Betriebssystemen (Windows, macOS, Gnome, KDE, etc.) vom Benutzer frei skaliert werden können und auch niedrige Auflösungen weit verbreitet sind, sollten dort lieber mehr als weniger Auflösungen vorbereitet werden.

Plattform Auflösungen (Kantenlänge in Pixeln) Dateiformat
Desktop 16, (20), 24, 32, (40), 48, (64), 128, 512 ICO
Favicon 16, (20), (24), 32, (128) ICO/PNG
Android Web App 192, 512 PNG
Apple Touch Icon 152, 167, 180 PNG

Auflösungen in Klammern sind optional, können das Ergebnis aber verbessern.

Entwurfsgitter #

Ein Icon beginnt im Design mit einer Vektorgrafik, die sich frei skalieren lässt, ohne dass Artefakte oder Aliasing auftreten. Diese Vektorgrafik wird dann in Bitmaps mit verschiedenen Auflösungen umgewandelt (gerastert). Damit solch ein Bitmap möglichst scharf erscheint, müssen vertikale und horizontale Kanten und Linien der Vektorgrafik möglichst genau auf die Pixelkanten fallen. Sonst treten im gerasterten Bitmap Unschärfen auf. Um scharfe Bitmaps in unterschiedlichen Auflösungen zu erhalten, werden daher mehrere Varianten der Vektorgrafik benötigt.

Die üblichen Auflösungen für die Bitmaps sind Vielfache von 16, 20 und 24. (Abgesehen von den Apple-Touch-Icons mit 152 und 167 Pixeln.) Beim Design genügt es daher in der Regel mit drei Entwurfsgittern mit dem einer vertikalen und horizontalen Aufteilung von 16, 20 und 14 zu arbeiten. Die vertikalen und horizontalten Linien der Vektorgrafik werden dann am Gitter ausgerichtet.

Hat das Motiv des Icons kontrastreiche Linien, z. B. einen Buchstaben oder Umrissformen, kann es sich lohnen, für höhere Auflösungen (64, 72, 80) zusätzliche Varianten mit dünneren Linien zu gestalten.

Die folgende Grafik zeigt die Vektorgrafiken für ein Icon, ausgerichtet auf verschiedenen Entwurfsgittern.

Entwurfsgitter
Entwurfsgitter

Die folgende Tabelle kann dabei helfen herauszufinden, welche Bitmap-Auflösung mit welchem Entwurfsgitter erzeugt werden kann.

Priorität Entwurfsgitter Stärke Bitmapauflösungen
1 16 normal 16, 32, 64, 128, (192), (256), 512
2 20 normal 20, 40, 80, (120), (160), (180), (200)
2 24 normal 24, 48, 72, (96), (144), (192)
3 64 schlank 64, 128, (192), (256), 512
4 72 schlank 72, (144)
4 80 schlank 80, (160)

Bitmap-Auflösungen in Klammern sind i. d. R. nicht notwendig, da sie ohne wesentlichen Qualitätsverlust aus höheren Auflösungen interpoliert werden können.

Ungewöhnliche Auflösungen, wie die der Apple-Touch-Icons, können aus der Vektorgrafik mit dem feinsten Entwurfsgitter gerastert werden. Dabei treten zwar Unschärfen auf, diese fallen jedoch nur wenig auf, da die Auflösungen vergleichsweise hoch sind.

Ein guter Kompromiss zwischen Gestaltungsaufwand und Icon-Qualität ist die folgende Auswahl an Vektorvarianten und Auflösungen.

Entwurfsgitter Stärke Bitmapauflösungen
16 normal 16, 32
20 normal 20, 40
24 normal 24, 48
64 schlank 64, 128, (167, 180), (192), 512

Werkzeuge #

Um die Vektorgrafik(en) des Icons zu gestalten, wird das OpenSource-Programm Inkscape10 verwendet.

Inkscape kann eine Vektorgrafik in einer beliebigen Auflösungen als Rastergrafik exportieren. Der Export kann entweder manuell über die grafische Benutzeroberfläche oder mit einem Skript über die Befehlszeile (CLI) ausgelöst werden. Inkscape unterstützt dabei u. .a. auch das PNG-Format. Das ICO-Format wird allerdings nicht direkt unterstützt.

Um die exportierten Rastergrafiken in ein Format zu übersetzen, das von Inkscape nicht unterstützt wird, z. B. ICO, wird das Befehlszeilenwerkzeug ImageMagick11 verwendet. Auch um die Schärfe von Rastergrafiken leicht zu verbessern, wird ImageMagick genutzt.

Ein Skript in der Programmiersprache Python12 bereitet die SVG-Datei aus Inkscape für verschiedene Export-Durchläufe vor und automatisiert den gesamten Ablauf.

Technik #

Inkscape speichert Vektorgrafiken in SVG-Dateien13. SVG-Dateien sind eine konkrete Ausprägung von XML-Dateien14. Inkscape macht sich das Konzept von XML-Namensräumen15 zunutze und erweitert SVG-Dateien mit eigenen Informationen, ohne vom SVG-Standard abzuweichen. Das Aussehen der grafischen Elemente in einer SVG-Datei wird mit Cascading Style Sheets (CSS)16 definiert. Die CSS-Definitionen in einer SVG-Datei ähneln denen in HTML-Seiten. CSS-Definitionen können entweder direkt für ein grafisches Objekt, oder für eine ganze Klasse von Objekten definiert werden. Inkscape ermöglicht es, CSS-Definitionen im Panel Selektoren und CSS direkt zu bearbeiten.

Das hier vorgestellte Verfahren basiert darauf, dass es relativ einfach ist, eine XML-Datei, und damit auch SVG-Dateien, in Python zu manipulieren. Konkret wird ein zusätzliches style-Tag an die SVG-Datei angefügt, welches dem Dokument zusätzliche CSS-Definitionen anhängt. Da diese am Ende des Dokumentes stehen, ergänzen und überschreiben sie die vorher mit Inkscape festgelegten Definitionen. CSS-Definitionen die von Inkscape direkt an einem Objekt festgelegt werden, und damit eine hohe Priorität haben, können dabei mit der Direktive !important überstimmt werden.

Indem CSS-Definitionen für die Sichtbarkeit der Ebenen angefügt werden, können gezielt unterschiedliche Ebenen für verschiedene Auflösungen sichtbar gemacht werden.

Für den Export, also die Umwandlung der Vektorgrafik in ein Bitmap im PNG-Format, wird Inkscape als Befehlszeilenprogramm aufgerufen. Dabei kann die SVG-Datei auch nebenbei in Inkscape geöffnet sein.

> inkscape --export-filename=<PNG-Datei> --export-width=<Größe in Pixel> <SVG-Datei>

Um die Schärfe der gerasterten Bitmaps noch leicht anzuheben, wird der convert-Befehl von ImageMagick mit der Option -sharpen verwendet. Die PNG-Datei wird hier mit dem geschärften Ergebnis überschrieben.

> magick convert <PNG-Datei> -sharpen 0x0.4 <PNG-Datei>

Um aus mehreren PNG-Dateien, die das Icon in unterschiedlichen Auflösungen enthalten, eine ICO-Datei zu erzeugen, wird ebenfalls ImageMagick genutzt.

> magic convert <PNG-Datei 1> <PNG-Datei 2> ... <ICO-Datei>

Icon-Gestaltung #

Die verschiedenen Varianten der Vektorgrafik werden in einer einzigen SVG-Datei angelegt. Dafür sind einige Vorbereitungen zu treffen:

  • Panel Dokumenteneinstellungen → Anzeige
    • Format: Benutzerdefiniert, px
    • Breite: 240,0
    • Höhe: 240,0
    • Anzeigeeinheiten: px
    • Skalierung: 1,0
    • Schachbrett: aktiv
  • Panel Ebenen und Objekte → Ebenen anlegen
    • R16, R20, R24 und R64
    • Die Namen müssen im Panel Objekteigenschaften in das Feld Kennung und Beschriftung eingetragen werden.
  • Panel Dokumenteneinstellungen → Gitter
    • Erzeugen → Neues Gitter: Rechteckig (4 mal)
    • Für alle Gitter
      • Gitter-Rastereinheiten: px
      • Ursprung X und Y: 0,0
      • Aktiv: ja
      • Sichtbar: ja
      • Nur an sichtbaren Gitterlinien einrasten: nein
    • Gitter 1 für R16
      • Abstand X und Y: 15,0
      • Hauptgitterlinien alle: 4
    • Gitter 2 für R20
      • Abstand X und Y: 12,0
      • Hauptgitterlinien alle: 5
    • Gitter 3 für R24
      • Abstand X und Y: 10,0
      • Hauptgitterlinien alle: 6
    • Gitter 4 für R64
      • Abstand X und Y: 3,75
      • Hauptgitterlinien alle: 8

Eine Vorlage ist unter https://github.com/mastersign/icon-dev/blob/dev/template.svg zu finden und kann von dort heruntergeladen werden.

Zunächst werden die Ebenen R20, R24 und R64 ausgeblendet. Und im Panel Dokumenteneinstellungen wird für das Gitter 1 der Haken bei Aktiv gesetzt, während für alle anderen Gitter der Haken entfernt wird. Es sollte sicher gestellt werden, dass das Einrasten für Gitterlinien aktiv ist. Zur Steuerung dient eine Schaltfläche und ein Menü in der Symbolleiste ganz rechts oben.

Nun kann das Icon auf der Ebene R16 und mit dem Gitter 1 gestaltet werden. Wenn das Icon auf der Ebene R16 gestaltet ist und die wichtigen horizontalen und vertikalen Linien am Gitter 1 ausgerichtet sind, können die Objekte aus der Ebene R16 in die übrigen Ebenen kopiert werden.

Für jede weitere Ebene wird die Ebene eingeblendet und alle anderen ausgeblendet, dann wird das dazugehörige Gitter aktiviert und alle anderen Gitter deaktiviert. Dann müssen nur die Formen am jeweiligen Gitter ausgerichtet werden.

Objekte für die die Kantenschärfe nicht wichtig ist, können auf einer oder mehreren zusätzlichen Ebenen und ohne Gitter gestaltet werden. In der Vorlage ist die zusätzliche Ebene BG angelegt.

Automatisierung #

Für den automatisierten Export in PNG- und ICO-Dateien wird ein Python-Skript verwendet.

Der Export des Icons in PNG-Dateien mit verschiedenen Auflösungen kann auch über den Mehrfachexport (Panel Exportieren → Reiter Mehrfachexport) erfolgen. Allerdings eröffnet der Weg über ein Python-Skript weitere interessante Möglichkeiten. Das Erzeugen einer ICO-Datei ist mit dem Mehrfachexport nicht möglich.

Das Python-Skript ist zusammen mit der SVG-Vorlage template.svg und dem Beispiel-Icon example.svg auf GitHub zu finden. Das Python-Skript heißt generate_icons.py.

Konfiguration #

Das Skript wird mit Hilfe einer JSON-Datei config.json konfiguriert. Die Konfiguration enthält den Pfad der SVG-Datei mit dem Icon svg_file, Pfade für Arbeits- und Ausgabeverzeichnis working_dir/output_dir, eine Liste mit Auflösungen und jeweils sichtbaren Ebenen resolutions und einiges mehr. Im praktischen Einsatz kann die config.json kopiert und angepasst werden.

{
    "svg_file": "example.svg",
    "working_dir": "tmp",
    "output_dir": "out",
    "resolutions": [
        {
            "layers": ["BG", "R16"],
            "sizes": [16, 32]
        },
        {
            "layers": ["BG", "R20"],
            "sizes": [20, 40]
        },
        {
            "layers": ["BG", "R24"],
            "sizes": [24, 48]
        },
        {
            "layers": ["BG", "R64"],
            "sizes": [64, 128, 152, 167, 180, 192, 512]
        }
    ],
    "sharpen": 0.4,
    "png_files": {
        "icon-32": 32,
        "icon-64": 64,
        "icon-512": 512,
        "apple-touch-icon-ipad-retina": 167,
        "apple-touch-icon-iphone-retina": 180,
        "android-web-app": 192
    },
    "ico_files": {
        "desktop_icon": [16, 20, 24, 32, 40, 48, 64, 128, 512],
        "favicon": [16, 20, 24, 32, 128]
    }
}

Imports #

Das Python-Skript beginnt mit dem Import verschiedener Standard-Pakete.

from os import getcwd
from pathlib import Path
from shutil import copy
from subprocess import run
import json
import sys
import xml.etree.ElementTree as XML

Mit der Funktion getcwd() aus dem Paket os kann das aktuelle Arbeitsverzeichnis ermittelt werden. Die Klasse Path aus dem Paket pathlib wird für das Zusammensetzen und Prüfen von Dateisystempfaden verwendet. Die Funktion copy() aus dem Paket shutil dient dem Kopieren von Dateien von einem Verzeichnis in ein anderes. Die Funktion run() aus dem Paket subprocess wird genutzt, um Befehlszeilenprogramme (Inkscape und ImageMagick) aufzurufen. Das Paket json wird beim Einlesen einer Konfigurationsdatei verwendet. Aus dem Paket sys wird die Variable argv genutzt, um Befehlszeilenargumente abzufragen. Und zu guter Letzt wird das Paket xml.etree.ElementTree unter dem Namen XML importiert. Es dient dem Parsen und Manipulieren der SVG-Datei.

Globale Variablen #

Nach den Imports werden globale Variablen für die Programmdateien von Inkscape und ImageMagick definiert. Sollte die beiden Programme nicht in der Umgebungsvariable PATH gelistet sein, können hier absolute Pfade eingetragen werden.

inkscape_executable = "inkscape"
imagemagick_executable = "magick"

Für das Bearbeiten der SVG-Datei werden zwei XML-Namensräume benötigt. Sie werden zunächst in einzelnen Variablen, beginnend mit NS_, gespeichert und dann im Dictionary NS mit XML-Präfixen verknüpft.

NS_SVG = "http://www.w3.org/2000/svg"
NS_INKSCAPE = "http://www.inkscape.org/namespaces/inkscape"
NS = {
    "svg": NS_SVG,
    "inkscape": NS_INKSCAPE,
}

Hilfsfunktionen #

Nun folgen eine Reihe von Hilfsfunktionen.

Die Funktion svg_layers() nimmt ein XML-Dokument entgegen und sucht darin alle Gruppen (svg:g-Tags) mit dem Attribut inkscape:groupmode gleich layer. Diese SVG-Gruppen repräsentieren die Ebenen in Inkscape. Anschließend werden die Werte des id-Attributs der Gruppen ausgelesen und in einem Set zurückgegeben. Sie entsprechen den Kennungen der Ebenen in Inkscape.

def svg_layers(svg: XML.ElementTree) -> set[str]:
    layers = svg.findall(".//svg:g[@inkscape:groupmode='layer']", NS)
    return set(l.attrib["id"] for l in layers)

Die Funktion build_layer_style() nimmt zwei Sets mit Ebenenkennungen entgegen. Das erste Set enthält die Kennungen aller Ebenen aus der SVG-Datei. Das zweite Set enthält nur die Kennungen jener Ebenen, die im Export sichtbar sein sollen. Durch Subtraktion der einen Menge von der anderen, wird ein Set gebildet, welches die Kennungen aller Ebenen enthält, die nicht im Export sichtbar sein sollen. Für jede ein- und auszublendende Ebene wird ein CSS-Style-Block gebildet: #Kennung { display: ... !important; }. Der Block setzt jeweils nur die display-Eigenschaft für die Gruppe. Für die einzublendenden Ebenen wird inline verwendet. Und für die auszublendenden Ebenen wird none verwendet. Die nachgestellte Direktive !important sorgt dafür, dass die display-Eigenschaft überschrieben wird, auch wenn sie bereits direkt am Gruppen-Tag der Ebene gesetzt wurde.

def build_layer_style(all_layers: set[str], include_layers: set[str]) -> str:
    exclude_layers = all_layers - include_layers
    include_layer_style = "\n".join(
        f"#{id} {{ display: inline !important; }}" for id in include_layers)
    exclude_layer_style = "\n".join(
        f"#{id} {{ display: none !important; }}" for id in exclude_layers)
    return "\n".join([include_layer_style, exclude_layer_style])

Der Aufruf build_layer_style({"A", "B", "C"}, {"B"}) erzeugt so den folgenden CSS-Code:

#B { display: inline !important; }
#A { display: none !important; }
#C { display: none !important; }

Die Funktion update_svg_style() aktualisiert den Inhalt eines svg:style-Tags im übergebenen XML-Dokument. Das svg:style-Tag wird mit einer übergebenen ID identifiziert. Wird es nicht gefunden, wird es am Ende des Dokumentes angefügt.

def update_svg_style(svg: XML.ElementTree, style: str, id: str = "icondev"):
    style_node = svg.find(f".//svg:style[@id='{id}']", NS)

    if style_node is None:
        style_node = XML.SubElement(svg.getroot(), XML.QName(NS_SVG, "style").text)
        style_node.set("id", id)

    style_node.text = style

Die Funktion run_command() Kapselt den Aufruf der run()-Funktion aus dem Paket subprocess. Ihre Aufgabe ist es, das Ergebnis des Befehlszeilenaufrufs zu überprüfen und True oder False zurückzugeben, je nach dem ob der Aufruf erfolgreich war oder nicht. Zusätzlich fängt run_command() die Fehlerausgabe des Befehlszeilenaufrufs auf und gibt sie aus. Die Standardausgabe wird ignoriert. Wird der Funktion True für den optionalen Parameter exit_on_error übergeben, wird das Python-Skript sofort mit dem Exit-Code 1 beendet wenn der Befehlszeilenaufruf scheitert.

def run_command(command: list[str], exit_on_error: bool = True) -> bool:
    result = run(command, capture_output=True)
    if result.returncode > 0 or result.stderr:
        print(result.stderr.decode(errors="replace"), file=sys.stderr)
        if exit_on_error:
            exit(1)
        else:
            return False
    return True

Befehlszeilen #

Die drei Funktionen raster_command(), sharpen_command() und ico_command() bilden mit Hilfe der übergebenen Parameter jeweils eine Befehlszeile und geben diese in Form einer Zeichenkettenliste zurück.

  • Die Befehlszeile von raster_command() ruft Inkscape auf, um ein SVG in ein PNG umzuwandeln
  • Die Befehlszeile von sharpen_command() ruft ImageMagick auf, um ein PNG nachzuschärfen
  • Die Befehlszeile von ico_command() ruft ImageMagick auf, um aus einer Reihe von PNG-Dateien eine ICO-Datei zu bauen
def raster_command(src_svg_file: Path, size: int, target_png_file: Path) -> list[str]:
    return [
        inkscape_executable,
        f"--export-width={size}",
        f"--export-filename={target_png_file}",
        str(src_svg_file),
    ]

def sharpen_command(file: Path, sharpen: float) -> list[str]:
    return [
        imagemagick_executable,
        "convert",
        str(file),
        "-sharpen",
        f"0x{sharpen}",
        str(file),
    ]

def ico_command(src_dir: Path, target_dir: Path, sizes: list[int], name: str) -> list[str]:
    return [
        imagemagick_executable,
        "convert",
        *(str(src_dir / f"{s}.png") for s in sizes),
        str(target_dir / f"{name}.ico"),
    ]

Kernfunktionen #

Die Funktion generate_bitmaps() arbeitet die Liste der Auflösungen ab. Die Auflösungen sind in der Konfigurationsdatei in Gruppen definiert.

...
"resolutions": [
    {
        "layers": ["BG", "R16"],
        "sizes": [16, 32]
    },
    {
        "layers": ["BG", "R20"],
        "sizes": [20, 40]
    },
    ...
]
...

Jede Auflösungsgruppe besitzt eine Liste mit Ebenenkennungen layers und einer Liste mit Auflösungen sizes. Die erste Liste gibt an, welche Ebenen sichtbar sein sollen. Und die zweite, welche Auflösungen mit den sichtbaren Ebenen gerastert werden sollen.

Für jede Auflösungsgruppe wird das übergebene SVG-Dokument mit einem CSS-Style-Block modifiziert. Das modifizierte SVG-Dokument wird in eine temporäre Datei im Arbeitsverzeichnis gespeichert und mit mehreren Befehlszeilenaufrufen von Inkscape in PNG-Dateien mit den verschiedenen Auflösungen umgewandelt. Wenn gewünscht, werden die PNG-Dateien noch etwas nachgeschärft.

def generate_bitmaps(
        svg: XML.ElementTree,
        tmp_dir: Path,
        resolutions: list[dict],
        sharpen: float | None):

    # Inkscape-Ebenen ermitteln
    all_layers = svg_layers(svg)

    # Auflösungen abarbeiten
    for res in resolutions:
        include_layers = set(res["layers"])

        # SVG vorbereiten
        layer_style = build_layer_style(all_layers, include_layers)
        update_svg_style(svg, layer_style, id="layer-visibility")
        tmp_svg_file = tmp_dir / "styled.svg"
        svg.write(str(tmp_svg_file))

        # SVG in verschiedenen Bildgrößen rastern
        sizes = res["sizes"]
        for size in sizes:
            filename = tmp_dir / f"{size}.png"
            if filename.exists():
                continue
            print("Bitmap mit", size, "Pixeln Kantenlänge rastern. Ebenen:",
                  ", ".join(include_layers))
            run_command(raster_command(tmp_svg_file, size, filename))
            # Bei Bedarf Bitmap nachschärfen
            if sharpen is not None and sharpen > 0.0:
                run_command(sharpen_command(filename, sharpen))

Die Funktionen copy_png_files() kopiert PNG-Dateien aus dem temporären Verzeichnis, entsprechend der Konfiguration png_files, unter neuem Namen in das Ausgabeverzeichnis.

def copy_png_files(tmp_dir: Path, target_dir: Path, png_files: dict):
    for name, size in png_files.items():
        print("PNG:", name)
        copy(tmp_dir / f"{size}.png", target_dir / f"{name}.png")

Die Funktion build_ico_files() arbeitet die Konfiguration ico_files ab und kombiniert mehrfach PNG-Dateien aus dem Arbeitsverzeichnis zu einer ICO-Datei im Ausgabeverzeichnis.

def build_ico_files(tmp_dir: Path, target_dir: Path, ico_files: dict):
    for name, sizes in ico_files.items():
        print("ICO:", name)
        run_command(ico_command(tmp_dir, target_dir, sizes, name))

Hauptprogramm #

Das Hauptprogramm befindet sich am Ende des Skripts. Zunächst werden zwei Dateisystempfade ermittelt. Die Variable project_root wird auf den Pfad des aktuellen Arbeitsverzeichnisses festgelegt. Das erste Befehlszeilenargument, welches an das Skript übergeben wird, wird als Pfad zur Konfigurationsdatei interpretiert. Wird kein Befehlszeilenargument übergeben, wird die Konfiguration aus config.json gelesen.

Dann liest das Programm die Konfiguration ein, bereitet das Arbeits- und Ausgabeverzeichnis vor und lädt die SVG-Datei. Anschließend extrahiert es einige Parameter aus der Konfiguration. zum Schluss ruft es mit diesen die Kernfunktionen auf, in denen die eigentliche Arbeit verrichtet wird.

if __name__ == "__main__":
    project_root = Path(getcwd())

    config_file = Path(sys.argv[1] if len(sys.argv) > 1 else "config.json")
    if not config_file.is_absolute():
        config_file = project_root / config_file

    # Konfiguration einlesen
    with open(config_file, "rb") as f:
        config = json.load(f)

    # Temporäres Verzeichnis vorbereiten
    tmp_dir = Path(config["temp_dir"])
    if not tmp_dir.is_absolute():
        tmp_dir = project_root / tmp_dir
    tmp_dir.mkdir(exist_ok=True)
    for f in tmp_dir.glob("*"):
        f.unlink()

    # Ausgabeverzeichnis vorbereiten
    out_dir = Path(config["output_dir"])
    if not out_dir.is_absolute():
        out_dir = project_root / out_dir
    out_dir.mkdir(exist_ok=True)

    # SVG laden
    svg_file = Path(config["svg_file"])
    if not svg_file.is_absolute():
        svg_file = project_root / svg_file
    svg = XML.parse(str(svg_file))

    # Konfiguration extrahieren
    resolutions = config["resolutions"]
    sharpen = config["sharpen"]
    png_files = config["png_files"]
    ico_files = config["ico_files"]

    # Bitmaps rastern, schärfen und im Ausgabeverzeichnis ablegen
    generate_bitmaps(svg, tmp_dir, resolutions, sharpen)
    copy_png_files(tmp_dir, out_dir, png_files)
    build_ico_files(tmp_dir, out_dir, ico_files)

Aufruf #

Wird das Skript wie folgt aufgerufen, arbeitet es die Konfiguration in config.json ab und lädt das Icon aus der Datei example.svg.

> python3 generate_icons.py config.json

Das Ergebnis sind die folgenden Dateien im Ausgabeverzeichnis:

  • android-web-app.png (192 Pixel)
  • apple-touch-icon-ipad-retina.png (167 Pixel)
  • apple-touch-icon-iphone-retina.png (180 Pixel)
  • desktop_icon.ico (16, 20, 24, 32, 40, 48, 64, 128, 512 Pixel)
  • favicon.ico (16, 20, 24, 32, 128 Pixel)
  • icon-32.png (32 Pixel)
  • icon-64.png (64 Pixel)
  • icon-512.png (512 Pixel)

Auflösungsübersicht #

Mit dem ebenfalls auf GitHub verfügbaren Skript generate_overview.py kann eine Übersicht mit verschiedenen Auflösungen des Icons erzeugt werden. Es nutzt dazu die verschiedenen Unterbefehle von ImageMagick.

Icon in mehreren Auflösungen
Icon in mehreren Auflösungen

Zusammenfassung #

In diesem Artikel wurde gezeigt, wie mit Inkscape ein Icon im SVG-Format gestaltet werden kann, dass auf unterschiedlichen Plattformen mit optimaler Schärfe dargestellt wird. Eine Kombination aus Ebenen und Gittern unterstützt bei der Gestaltung von drei bis sechs Varianten der Vektorgrafik. Jede Variante wird für den Export von mehreren Bitmaps in unterschiedlichen Auflösungen genutzt. Ein Python-Skript automatisiert den Export der SVG-Datei in PNG- und ICO-Dateien. Dabei kommt ImageMagick zum Einsatz, um die Bitmaps leicht nachzuschärfen und die ICO-Dateien zu erstellen. Das Python-Skript wird mit Hilfe einer JSON-Datei konfiguriert.

Ausblick #

Im einem anschließenden Artikel sollen die Möglichkeiten, die sich durch die CSS-Stile in einer Inkscape-SVG-Datei und der hier vorgestellten Technik eröffnen, weiter erläutert werden. Insbesondere ist von Interesse, wie aus einer SVG-Datei automatisiert verschiedenfarbige Varianten erzeugt werden können.