Python: Web-Fonts für Publii extrahieren

Die aktuelle Version dieses Blogs entsteht mit Hilfe des Open Source Content Management Systems Publii v.0.33.4 (build 11590) Beta. Welches sich dadurch auszeichnet, dass es statische Web-Seiten erzeugt, die ganz ohne MySQL-Datenbank oder PHP-Scripting im Back-End praktisch von jedem HTTP-Server aus dargestellt werden können. Das macht die Nutzung einfachster Webspace Angebote wie auch das meines Providers praktisch erst sinnvoll.

Wenn da nicht die Sache mit den Web-Schriftarten wäre.

Aufgrund der Anforderungen, die sich aus der Datenschutzerklärung ergeben, habe ich natürlich auch den Anspruch, dass externe Quellen jeweils durch Öffnen in einem neuen Fenster/Tab deutlich von meiner Domain abgegrenzt werden. Das Einbetten von Inhalten anderer Domains würde daher nicht nur irgendwie dem Prinzip der statischen Web-Seiten widersprechen, der Benutzer hätte zudem noch (ohne technische Vorkehrungen z.B. in seinem Browser) keinerlei Möglichkeit, dies einfach zu unterbinden.

Also prüfe ich für ihn die geladenen Inhalte meines Blogs mit dem Firefox Debugger und finde leider heraus, dass ein Heebo-CSS von fonts.googleapis.com sowie etwas später 2 WOFF2-Schriftarten von fonts.gstatic.com zusätzlich zu den Ressourcen meiner Web-Seite abgerufen werden.

Oh je! Aber eventuell findet sich ja ein Weg, dieses Cross-Origin Resource Sharing genannte Verhalten zu unterbinden.

Um also heraus zu finden, wo diese Quellen referenziert werden, sucht man im Publii-Verzeichnis mit einem Werkzeug wie Total Commander nach Dateien mit dem Inhalt fonts.. Dies dürfte sowohl Referenzen auf fonts.googleapis.com als auch auf fonts.gstatic.com aufdecken. Dabei rate ich aber grundlegend von der Suche mit dem Windows Explorer ab, weil dieser unterschlägt, was wirklich interessant ist und in meinem Fall völlig zu Unrecht visual-override.js aus dem tattoo Theme verdächtigt.

Viel wichtiger ist aber die Erkenntnis, dass in jedem mitgelieferten Theme unter partials\ mit head.hbs genau eine Datei zu finden ist, in dem fonts. vorkommt.

Da ich das mercury Theme verwende, konzentriere ich mich nachfolgend auch nur auf dieses. Es wird dabei das mitgelieferte Theme unter themes\ so belassen wie es ist und nur an dem Theme, welches durch die Konfiguration per Publii-Front-End in mein Blog-Verzeichnis hineinkopiert wurde, herum geändert.

Hier exemplarisch unter c:\Users\mike\Documents\Publii\sites\blog-von-mike-ziebeck\input\themes\mercury\ suche ich in partials\head.hbs per Editor die verdächtige Stelle.

Die beiden Zeilen:

<link rel="preconnect" href="https://fonts.gstatic.com/" crossorigin>

<link href="https://fonts.googleapis.com/css?family=Heebo:400,500%7CPlayfair+Display:400" rel="stylesheet">

Bedeuten soviel wie fonts.gstatic.com für weitere Anfragen bereitmachen und Cascading Style Sheet (der Einfachheit halber nur Heebo-CSS genannt) von https://fonts.googleapis.com/css?family=Heebo:400,500%7CPlayfair+Display:400 nachladen und einbinden.

Ich will beides nicht, daher wird die erste Zeile ersatzlos gestrichen und die zweite Zeile geändert in:

<link rel="stylesheet" href="{{css "heebo.fonts.googleapis.com.css" }}">

So soll das Heebo-CSS statt von Google von jetzt an von meinem Server (zu finden unter assets/css/heebo.fonts.googleapis.com.css) geladen werden. Damit das aber auch funktioniert, muss selbiges zunächst von https://fonts.googleapis.com/css?family=Heebo:400,500%7CPlayfair+Display:400 herunter geladen und im CSS-Verzeichnis des mercury Themes als heebo.fonts.googleapis.com.css gespeichert werden.

Wie anhand des Icons für CSS-Dateien zu sehen, nehme ich Eclipse als Standard Editor für Cascading Style Sheets. Doppelklick auf das Heebo-CSS und man sieht im CSS-Editor gleich 8 Referenzen auf externe WOFF2-Schriftarten.

Nun braucht man eigentlich „nur“ noch die 8 in url() eingeklammerten Links herunter zu laden und im Heebo-CSS selbige gegen die Pfade der heruntergeladenen Dateien zu tauschen und fertig.

Aber Spaß sieht irgendwie anders aus!

Insbesondere wenn vielleicht einmal andere Themes zur Anwendung kommen, die dann wieder neue Web-Schriftarten referenzieren… Wäre es da nicht schöner, ein kleines Script zu haben, dass dabei zur Hand gehen kann?


Da ich nun schon an anderer Stelle von Python geschwärmt habe, liegt es sicherlich nahe, diese Aufgabe mit eben dieser Programmiersprache zu lösen. Und damit das auch einfach funktioniert, beachtet man am besten ein paar Tipps zur Einrichtung unter Windows 10, die ich im Artikel [Python: Entwicklung unter Windows 10] genauer beschreibe.


Für unser publii_extract_fonts.py Script setze ich nun funktionierendes Drag & Drop über den Windows Explorer voraus und fange mit folgendem Code an.

1
2
3
4
5
6
7
8
'''
Created on 22.03.2019
@author: Mike Ziebeck
'''
import sys

print(sys.argv)
input("Hit ENTER to finish.")

Per Explorer wird jetzt das Heebo-CSS auf publii_extract_fonts.py gezogen.

Und wie erwartet, werden alle Kommandozeilen Parameter des Aufrufes ausgegeben. Der erste Parameter beschreibt dabei den kompletten Pfad des publii_extract_fonts.py Scripts und alle per Drag & Drop abgelegten Dateien folgen danach.

Um also für jedes abgelegte CSS die entsprechenden Web-Schriftarten zu extrahieren, definiere ich eine Funktion extract_fonts_from_css und führe sie für jedes CSS aus. Das fertige Script sieht dann so aus:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
'''
Created on 22.03.2019
@author: Mike Ziebeck

Extrahiert Web-Fonts und transformiert das referenzie-
rende CSS sodaß sie vom gleichen Server geladen werden
können statt z.B. von Google.

Das neue CSS wird dabei *.new.css benannt und die
heruntergeladenen Fonts werden unter PATH relativ zum
CSS-Verzeichnis abgelegt.
'''
import re, os, wget, sys

def extract_fonts_from_css(css, PATH=["..", "fonts"]):
    print("Extracting fonts from CSS for file:", css)

    with open(css, "r") as in_file:
        css_data= in_file.read()
        url_list= re.findall("url\(([^)]+)\)", css_data,
                             re.I) #@UndefinedVariable
    
    for url in url_list:
        print(" processing url:", url)
        font_file= os.path.join( os.path.dirname(css), 
                                 *PATH, 
                                 os.path.basename(url) )
        print(" target file:", font_file)
        if not os.path.isfile( font_file):
            os.makedirs( os.path.dirname(font_file), 
                         exist_ok=True )
            font_file= wget.download(url, font_file)
            print("")
        font_path= PATH + [os.path.basename(font_file)]
        font_path_str= "/".join(font_path) 
        css_data= css_data.replace(url, font_path_str)
    
    with open( css+".new.css", "w+") as out_file:
        out_file.write(css_data)

for css_file in sys.argv[1:]:
    extract_fonts_from_css(css_file)

input("Hit ENTER to finish.")

Wesentliche Bestandteile des Quell-Textes beschreiben sich so:

Zunächst werden Bibliotheken für die Behandlung regulärer Ausdrücke (re), zur Verarbeitung von Datei-Pfaden (os), zum Herunterladen von Web-Inhalten mit wget (wget) und zum Ermitteln der Kommandozeilen-Parameter (sys) importiert. 

13
import re, os, wget, sys

Dann erfolgt die Funktionsdefinition extract_fonts_from_css mit Festlegung des Pfades für die heruntergeladenen Web-Schriftarten relativ zur Adresse des übergebenen CSS-Dateinamens css.

15
def extract_fonts_from_css(css, PATH=["..", "fonts"]):

Darauf folgt schon das Einlesen der CSS-Datei und es werden per regulärem Ausdruck alle URLs ermittelt, die via url(..) eingeklammert wurden. Der Kommentar #@UndefinedVariable wird dabei angewendet, um die anderenfalls auftretende PyDev-Fehlermeldung Undefined variable from import: I abzuschalten.

18
19
20
21
    with open(css, "r") as in_file:
        css_data= in_file.read()
        url_list= re.findall("url\(([^)]+)\)", css_data,
                             re.I) #@UndefinedVariable    

Im nächsten Abschnitt wird für jede gefundene URL die erwartete Schriftart-Datei (font_file) ermittelt. Wenn sie jedoch noch nicht lokal vorhanden ist (not os.path.isfile), wird das Schriftarten-Verzeichnis bei Bedarf erstellt (os.makedirs) und die Schriftart heruntergeladen (wget.download). Anschließend wird jedes Vorkommen der URL ersetzt (css_data.replace) durch die zuvor generierte lokale Referenz (font_path_str). 

23
24
25
26
27
28
29
30
31
32
33
34
35
36
    for url in url_list:
        print(" processing url:", url)
        font_file= os.path.join( os.path.dirname(css), 
                                 *PATH, 
                                 os.path.basename(url) )
        print(" target file:", font_file)
        if not os.path.isfile( font_file):
            os.makedirs( os.path.dirname(font_file), 
                         exist_ok=True )
            font_file= wget.download(url, font_file)
            print("")
        font_path= PATH + [os.path.basename(font_file)]
        font_path_str= "/".join(font_path) 
        css_data= css_data.replace(url, font_path_str)

Schlußendlich werden die Änderungen am Cascading Style Sheet in eine neue (css+".new.css") Datei im selben Verzeichnis wie das Eingangs CSS geschrieben.

38
39
    with open( css+".new.css", "w+") as out_file:
        out_file.write(css_data)

Ein Ablegen des Heebo-CSS auf das so erweiterte publii_extract_fonts.py Script erzeugt jetzt neben folgender Text Ausgabe:

.. auch die erwarteten WOFF2-Schriftarten unter assets\fonts:

Und im neu erstellten Heebo-CSS mit Namen heebo.fonts.googleapis.com.css.new.css finden sich nun auch die geänderten url(..) Referenzen wieder:

Da sowohl die heruntergeladenen Fonts an Ort und Stelle sind, wo wir sie haben wollen, als auch das neue Heebo-CSS den gewünschten Inhalt hat, können wir das alte Heebo-CSS jetzt auch mit dem Neuen ersetzen.

Und Oh Freude! Ein Aktualisieren des Blogs belohnt uns gleich mit sorgfältig von meinem Server heruntergeladenen Web-Schriftarten. Wie hier zur Kontrolle wieder mit dem Firefox-Debugger veranschaulicht: