Labor (Ruby)
image: title.webp kind: labor language: Ruby / Python languages: [ruby, python] duration: 2 Doppelstunden

Komprimieren mit LZW

Rhabarberbarbara, Barbarabar, Barbarbarbier: Wo sich etwas wiederholt, lässt es sich kürzer sagen. LZW merkt sich beim Lesen jede Folge von Bytes, die schon einmal vorkam, und schreibt beim nächsten Mal nur noch eine Nummer. Das Wörterbuch dafür wird nie mitgeschickt: Wer entpackt, baut es beim Lesen selbst noch einmal. Du schreibst beides selbst, in Ruby oder Python, den Kompressor und den Entpacker, und probierst sie an Texten, Fotos und der Spezifikation von GIF aus.

1. Wiederholungen

Ein Text wiederholt sich ständig: Buchstabenfolgen wie »ch«, »sch«, »ein« und »der« kommen auf jeder Seite dutzendfach vor, ganze Wörter auch. Ein Programm, das komprimiert, sucht solche Wiederholungen und ersetzt sie durch etwas Kürzeres. Beim Entpacken muss genau das Original zurückkommen, Byte für Byte; das heißt verlustfrei.

Das Verfahren auf dieser Seite haben Abraham Lempel und Jacob Ziv 1978 erfunden und Terry Welch 1984 so vereinfacht, wie es seitdem benutzt wird, daher der Name LZW. Es steckt in GIF-Bildern, in TIFF, in alten PDF-Dateien und im Unix-Programm compress. Die Idee ist ein Wörterbuch, in dem jede Folge von Bytes, die schon vorkam, eine Nummer hat, den Code:

  • Am Anfang stehen darin die 256 möglichen Bytes, jedes einzeln. Das Byte 65 (»A«) hat den Code 65.
  • Jedes Mal, wenn das Programm einen Code ausgibt, kommt ein neuer Eintrag dazu, mit dem nächsten freien Code: 256, 257, 258, …
  • Ein Code steht also für eine immer längere Folge, und wo sich etwas wiederholt, ersetzt ein einziger Code mehrere Bytes.

2. Komprimieren

Der Kompressor liest den Text Byte für Byte und merkt sich das Präfix, das er gerade aufbaut: den Anfang des restlichen Texts, der schon im Wörterbuch steht. Für jedes Byte gilt:

  1. Steht das Präfix mit dem neuen Byte dahinter schon im Wörterbuch, wird das Präfix um dieses Byte länger.
  2. Sonst gibt er den Code des Präfixes aus, trägt das Präfix mit dem neuen Byte als neuen Eintrag ein und beginnt ein neues Präfix, das nur aus dem neuen Byte besteht.
  3. Ist der Text zu Ende, gibt er den Code des letzten Präfixes aus.

Probier es aus. Jeder Klick liest ein Byte; das gelbe ist das Präfix, das rot umrandete das nächste Byte. Du kannst auch einen eigenen Text eintippen.

Bei der Rhabarberbarbara stehen nach dem ersten »BAR« für »BA«, »AR« und »RB« eigene Einträge im Wörterbuch, und schon das zweite »BARBARA« braucht weniger Codes als Buchstaben. Ganz am Anfang kann LZW nichts sparen: Das Wörterbuch kennt noch nichts außer den einzelnen Bytes.

Vermutung 1Von Hand

Komprimiere ABABABA von Hand, nach den drei Regeln, bevor du es oben in den Schrittmacher tippst. »A« ist das Byte 65, »B« das Byte 66. Welche Codes kommen heraus? Schreib sie mit Leerzeichen dazwischen, dann sieh im Schrittmacher nach.

Tipp

Schreib eine Tabelle mit den Spalten Präfix, Byte, Ausgabe und neuer Eintrag. Der erste neue Eintrag ist 256 = AB. Mit dem Schrittmacher kannst du danach nachsehen.

3. Einrichten

Lege im Terminal (StrgJ) einen Ordner an und öffne ihn mit StrgK StrgO:

mkdir /workspace/lzw

Lege darin die Datei lzw.rb an:

# LZW: komprimieren und entpacken

# Macht aus den Bytes eines Strings eine Liste von Codes.
def compress(data)
    codes = []
    codes
end

# Macht aus einer Liste von Codes wieder die Bytes, als String.
def decompress(codes)
    ''
end

p compress('ABABABA')

Ein String besteht aus Bytes, Zahlen von 0 bis 255: 'ABABABA' sind die Bytes 65, 66, 65, … Was unten außerhalb der Methoden steht, ist dein Programm: Es läuft, wenn du die Datei startest, mit ruby lzw.rb. Die Prüfungen der Aufgaben laden nur deine Methoden und rufen sie direkt auf; dein Programm läuft dabei nicht. Im Moment gibt das Programm eine leere Liste aus; das änderst du jetzt.

Lege darin die Datei lzw.py an:

# LZW: komprimieren und entpacken


def compress(data):
    """Macht aus Bytes eine Liste von Codes."""
    codes = []
    return codes


def decompress(codes):
    """Macht aus einer Liste von Codes wieder die Bytes."""
    return b''


print(compress(b'ABABABA'))

b'ABABABA' ist kein Text, sondern eine Folge von Bytes, Zahlen von 0 bis 255. Was unten außerhalb der Funktionen steht, ist dein Programm: Es läuft, wenn du die Datei startest, mit python3 lzw.py. Die Prüfungen der Aufgaben laden nur deine Funktionen und rufen sie direkt auf; dein Programm läuft dabei nicht. Im Moment gibt das Programm eine leere Liste aus; das änderst du jetzt.

4. Der Kompressor

Schreib compress nach den drei Regeln oben. Was du aus Ruby dafür brauchst:

  • data.each_byte { |byte| … } geht die Bytes eines Strings durch und liefert Zahlen: für 'AB' erst 65, dann 66.
  • Ein Präfix ist am einfachsten ein Array von Zahlen, etwa [65, 66] für »AB«. current + [byte] ist ein neues Array mit einer Zahl mehr.
  • Das Wörterbuch ist ein Hash mit solchen Arrays als Schlüsseln und Codes als Werten. Am Anfang enthält es für jede Zahl i von 0 bis 255 den Eintrag [i] mit dem Code i. Der nächste freie Code ist dann immer table.size, und table.key?(prefix) sagt, ob es ein Präfix prefix schon gibt.
  • Das Präfix am Anfang ist leer, []. Achte am Ende darauf, dass ein leerer Text keinen Code ergibt.

Schreib compress nach den drei Regeln oben. Was du aus Python dafür brauchst:

  • Eine for-Schleife über Bytes liefert Zahlen: for byte in b'AB' gibt erst 65, dann 66.
  • Aus einer Zahl wird mit bytes([byte]) wieder ein Byte, und + hängt Bytes aneinander: b'AB' + bytes([67]) ist b'ABC'.
  • Das Wörterbuch ist ein dict mit Bytes als Schlüsseln und Codes als Werten. Am Anfang enthält es für jede Zahl i von 0 bis 255 den Eintrag bytes([i]) mit dem Code i. Der nächste freie Code ist dann immer len(table).
  • Das Präfix am Anfang ist leer, b''. Achte am Ende darauf, dass ein leerer Text keinen Code ergibt.
Aufgabe 2Der Kompressor

Schreib compress in lzw.rb.

Tipp

Das Wörterbuch ist am Anfang 256.times { |i| table[[i]] = i }, mit table = {} davor. Ein Eintrag kommt immer dann dazu, wenn ein Code ausgegeben wird, und zwar das Präfix mit dem neuen Byte dahinter.

Aufgabe 2Der Kompressor

Schreib compress in lzw.py.

Tipp

Das Wörterbuch ist am Anfang {bytes([i]): i for i in range(256)}. Ein Eintrag kommt immer dann dazu, wenn ein Code ausgegeben wird, und zwar das Präfix mit dem neuen Byte dahinter.

5. Entpacken

Der Entpacker bekommt nur die Codes. Die Codes unter 256 versteht er sofort, sie sind einzelne Bytes. Für alle anderen baut er das Wörterbuch selbst nach, so wie der Kompressor es gebaut hat, nur einen Schritt später:

  1. Der erste Code ist immer ein einzelnes Byte. Schreib es hin; es ist der vorige Eintrag für den nächsten Schritt.
  2. Für jeden weiteren Code: Schlag den Eintrag nach und schreib ihn hin.
  3. Trag dann den vorigen Eintrag mit dem ersten Byte des jetzigen dahinter als neuen Eintrag ein. Genau diesen Eintrag hat der Kompressor angelegt, als er den vorigen Code ausgegeben hat, denn das nächste Byte war das erste Byte des jetzigen Eintrags.

Das Wörterbuch des Entpackers ist also immer einen Eintrag hinter dem des Kompressors. Meistens macht das nichts, aber nicht immer:

Vermutung 3Ein Code, den es noch nicht gibt

Der Entpacker bekommt 65 66 256 258. Nach 65, 66 und 256 kennt er die Einträge bis 257. Wofür steht 258?

Ein Code, den es noch nicht gibt

Der Kompressor kann einen Eintrag benutzen, den er gerade erst angelegt hat. Das passiert, wenn sich ein Präfix sofort wiederholt und dabei mit seinem eigenen ersten Byte weitergeht, wie bei ABABABA. Der Entpacker kennt diesen Eintrag noch nicht, aber er kann ihn ausrechnen: Es ist der vorige Eintrag mit dessen eigenem ersten Byte dahinter. Das ist die einzige Ausnahme von Regel 2.

Hier entpackt der Schrittmacher die Codes des Texts, der oben im Feld steht; die Ausnahme ist rot markiert. Tipp oben ABABABA oder AAAAAAAA ein und sieh hier zu.

Schreib jetzt decompress. Diesmal ist das Wörterbuch andersherum: ein Hash mit den Codes als Schlüsseln und den Einträgen, Arrays von Zahlen, als Werten. Sammle alle Zahlen in einem Array; entry[0] ist das erste Byte eines Eintrags. Am Schluss macht pack('C*') aus dem Array von Zahlen wieder einen String: [65, 66].pack('C*') ist 'AB'.

Schreib jetzt decompress. Diesmal ist das Wörterbuch andersherum: ein dict mit den Codes als Schlüsseln und den Bytes als Werten. entry[:1] ist das erste Byte eines Eintrags, als Bytes (entry[0] wäre eine Zahl). Am Schluss setzt b''.join(parts) eine Liste von Bytes wieder zu einer einzigen Folge zusammen.

Aufgabe 4Der Entpacker

Schreib decompress in lzw.rb. Was compress macht, muss decompress genau zurückgeben, für jede Folge von Bytes.

Tipp

Merk dir den vorigen Eintrag. Steht ein Code noch nicht im Wörterbuch, ist er genau table.size, und sein Eintrag ist der vorige mit dessen erstem Byte dahinter: previous + [previous[0]].

Aufgabe 4Der Entpacker

Schreib decompress in lzw.py. Was compress macht, muss decompress genau zurückgeben, für jede Folge von Bytes.

Tipp

Merk dir den vorigen Eintrag. Steht ein Code noch nicht im Wörterbuch, ist er genau len(table), und sein Eintrag ist der vorige mit dessen erstem Byte dahinter. entry[:1] ist das erste Byte als Bytes, entry[0] wäre eine Zahl.

6. Wie gut ist das?

Bisher zählen wir Codes. Gespeichert werden muss aber jeder Code mit so vielen Bits, wie er braucht, und die Codes werden immer größer. Beim i-ten Code (ab 0 gezählt) hat das Wörterbuch 256 + i Einträge, der größte mögliche Code ist also 255 + i. So viele Bits braucht der Code: In Ruby sagt das (255 + i).bit_length. Schreib eine Funktion size_in_bytes(codes), die alle Bits zusammenzählt und in Bytes umrechnet, aufgerundet.

Bisher zählen wir Codes. Gespeichert werden muss aber jeder Code mit so vielen Bits, wie er braucht, und die Codes werden immer größer. Beim i-ten Code (ab 0 gezählt) hat das Wörterbuch 256 + i Einträge, der größte mögliche Code ist also 255 + i. So viele Bits braucht der Code: In Python sagt das (255 + i).bit_length(). Schreib eine Funktion size_in_bytes(codes), die alle Bits zusammenzählt und in Bytes umrechnet, aufgerundet.

Lade dann zwei Dateien zum Ausprobieren in den Ordner: die Spezifikation von GIF, ein Text von 1990 mit 83.003 Bytes, und ein Foto als JPEG.

wget https://workspace.hackschule.de/lzw/spec-gif89a.txt
wget https://workspace.hackschule.de/jpeg/44-baseline.jpg

Eine Datei liest Ruby mit File.binread('spec-gif89a.txt') Byte für Byte, so wie sie ist. ARGV[0] ist der Name, den du beim Start mitgibst: ruby lzw.rb spec-gif89a.txt. Lass dein Programm für eine Datei ausgeben, wie groß sie ist, wie viele Codes compress daraus macht und wie viele Bytes die brauchen. Probier dabei auch gleich, ob decompress wieder genau die Datei ergibt.

Eine Datei liest Python mit open('spec-gif89a.txt', 'rb').read() als Bytes; das b in 'rb' ist wichtig, sonst bekommst du Text statt Bytes. sys.argv[1] ist der Name, den du beim Start mitgibst (python3 lzw.py spec-gif89a.txt), nach import sys. Lass dein Programm für eine Datei ausgeben, wie groß sie ist, wie viele Codes compress daraus macht und wie viele Bytes die brauchen. Probier dabei auch gleich, ob decompress wieder genau die Datei ergibt.

Aufgabe 5Die Spezifikation von GIF

Wie viele Bytes braucht spec-gif89a.txt nach size_in_bytes, also komprimiert, mit so vielen Bits pro Code wie nötig?

Tipp

(255 + i).bit_length Bits für den i-ten Code, alle zusammenzählen, durch 8 teilen und aufrunden. Lies die Datei mit File.binread.

Aufgabe 5Die Spezifikation von GIF

Wie viele Bytes braucht spec-gif89a.txt nach size_in_bytes, also komprimiert, mit so vielen Bits pro Code wie nötig?

Tipp

(255 + i).bit_length() Bits für den i-ten Code, alle zusammenzählen, durch 8 teilen und aufrunden. Lies die Datei mit open('spec-gif89a.txt', 'rb').read().

Echte Programme komprimieren noch besser. gzip benutzt LZ77, die erste Idee von Lempel und Ziv von 1977, und codiert das Ergebnis danach noch mit Huffman:

gzip -9 -k spec-gif89a.txt
ls -l spec-gif89a.txt*
spec-gif89a.txt Bytes
Original 83.003
LZW, mit wachsender Breite der Codes dein Ergebnis
gzip -9 20.271

Und das Foto?

Vermutung 6Ein Foto

Was passiert, wenn du 44-baseline.jpg (143.206 Bytes) mit deinem LZW komprimierst? Rate erst, dann probier es aus.

Ein Foto
Aufgabe 7Warum größer?

Warum wird das JPEG mit LZW größer statt kleiner? Erkläre es in eigenen Worten. Was müsste in einer Datei stehen, damit LZW viel spart?

Mindestens 25 Wörter. Nur du und deine Lehrkraft können die Antwort lesen.

7. Ein volles Wörterbuch

Unser Wörterbuch wächst ohne Grenze: Bei der Spezifikation hat es am Ende über 17.000 Einträge, bei einer großen Datei wären es Millionen. Echte Programme setzen eine Grenze. GIF erlaubt höchstens 4096 Einträge, also Codes mit höchstens 12 Bits; compress geht bis 16 Bits. Ist das Wörterbuch voll, kommen keine Einträge mehr dazu, oder es beginnt von vorn: Der Kompressor schickt dafür einen eigenen Code, den Clear Code, und beide Seiten fangen wieder mit den 256 Bytes an.

Bau das in deine beiden Funktionen ein: Reserviere den Code 256 als Clear Code, die neuen Einträge beginnen dann bei 257. Hat das Wörterbuch 4096 Einträge, gibt compress den Clear Code aus, und beide Seiten beginnen neu. Vergleich mit size_in_bytes, wie viel größer die Spezifikation dadurch wird, und überleg, warum ein frisches Wörterbuch bei einem langen Text sogar helfen kann.

8. Weiter geht's

  • GIF: In einem GIF-Bild sind die »Bytes« die Nummern der Farben, und die Codes stehen Bit für Bit in der Datei. Wie das genau aussieht, steht in Eine GIF-Datei decodieren, mit einem Decoder in C.
  • Das Patent: Auf LZW hatte die Firma Unisys ein Patent, und ab 1994 verlangte sie Lizenzgebühren von Programmen, die GIF-Dateien schrieben. Als Antwort entstand 1995 das Format PNG, das ohne LZW auskommt. Das Patent lief 2003 in den USA und 2004 in Europa aus.
  • Andere Texte: Komprimiere Texte in anderen Sprachen, eine Datei aus lauter gleichen Bytes, eine mit Zufallszahlen (head -c 100000 /dev/urandom > zufall.bin). Wann lohnt sich LZW, wann nicht?