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

Komprimieren mit Lauflängen

Sechsmal Weiß, dreimal Schwarz, viermal Weiß: Statt jedes Pixel einzeln aufzuschreiben, sagt man, wie lang jeder Lauf gleicher Pixel ist. So schickten Faxgeräte ihre Seiten durch die Telefonleitung, so speicherten frühe Bildformate ihre Bilder, und so packt JPEG die vielen Nullen eines Fotos zusammen. Du schreibst es selbst, in Ruby oder Python, und findest heraus, welche Dateien damit kleiner werden, und welche doppelt so groß.

1. Läufe

Eine Zeile eines Bildes aus wenigen Farben besteht meist aus langen Stücken derselben Farbe: links Hintergrund, dann ein Rand, dann eine Fläche. So ein Stück heißt Lauf, und statt »Weiß, Weiß, Weiß, Weiß, Weiß, Weiß« genügt »6 × Weiß«. Diese Lauflängencodierung (englisch Run-Length Encoding, RLE) ist das einfachste Verfahren zum Komprimieren, und sie ist verlustfrei: Aus den Läufen kommt genau die Zeile zurück.

Hier ist die Diskette aus dem GIF-Labor, 16 × 16 Pixel in 4 Farben. Neben jeder Zeile stehen ihre Läufe. Wähl eine Farbe und klick in das Bild, oder zieh mit gedrückter Maustaste: Die Läufe und die Größe ändern sich mit.

Jeder Lauf braucht zwei Bytes, eins für die Länge und eins für die Farbe. Die Diskette hat lange, ruhige Zeilen und wird viel kleiner. Mal ein Schachbrett hinein, und es wird größer als das Bild selbst: Jedes Pixel ist dann ein eigener Lauf und braucht zwei Bytes statt einem.

Aufgabe 1Von Hand

Wie sieht AAAABBBCCD als Lauflängen aus, mit der Zahl vor jedem Zeichen?

2. Einrichten

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

mkdir /workspace/rle

Lege darin die Datei rle.rb an:

# Lauflängen: statt »AAAA« steht »4A« da.

# Als Text: 'WWWWWWBBBWWWW' wird '6W3B4W'
def rle(text)
end

def unrle(code)
end

# Als Bytes: immer zwei Zahlen, die Länge (1 bis 255) und das Byte
def rle_bytes(data)
end

def unrle_bytes(bytes)
end

p rle('WWWWWWBBBWWWW')

Lege darin die Datei rle.py an:

# Lauflängen: statt »AAAA« steht »4A« da.


def rle(text):
    """Als Text: 'WWWWWWBBBWWWW' wird '6W3B4W'"""
    return ''


def unrle(code):
    return ''


def rle_bytes(data):
    """Als Bytes: immer zwei Zahlen, die Länge (1 bis 255) und das Byte"""
    return []


def unrle_bytes(numbers):
    return b''


print(rle('WWWWWWBBBWWWW'))

3. Als Text

Schreib rle. Es geht den Text Zeichen für Zeichen durch, zählt, wie oft dasselbe Zeichen hintereinander kommt, und schreibt für jeden Lauf die Zahl und das Zeichen hin.

  • text.each_char { |char| … } geht die Zeichen durch.
  • Merk dir das Zeichen des Laufs und seine Länge. Kommt ein anderes Zeichen, ist der Lauf zu Ende: Häng "#{count}#{char}" an das Ergebnis und beginne einen neuen Lauf.
  • Vergiss den letzten Lauf nicht, und dass ein leerer Text einen leeren Code ergibt.
  • for char in text: geht die Zeichen durch.
  • Merk dir das Zeichen des Laufs und seine Länge. Kommt ein anderes Zeichen, ist der Lauf zu Ende: Häng f'{count}{char}' an das Ergebnis und beginne einen neuen Lauf.
  • Vergiss den letzten Lauf nicht, und dass ein leerer Text einen leeren Code ergibt.
Aufgabe 2Lauflängen als Text

Schreib rle in rle.rb.

Tipp

Merk dir das Zeichen des aktuellen Laufs und zähle. Kommt ein anderes Zeichen, hängst du "#{count}#{char}" an und beginnst neu. Nach der Schleife fehlt noch der letzte Lauf.

Aufgabe 2Lauflängen als Text

Schreib rle in rle.py.

Tipp

Merk dir das Zeichen des aktuellen Laufs und zähle. Kommt ein anderes Zeichen, hängst du f'{count}{char}' an und beginnst neu. Nach der Schleife fehlt noch der letzte Lauf.

Dann unrle, andersherum: aus '6W3B4W' wieder 'WWWWWWBBBWWWW'. Eine Zahl kann mehrere Ziffern haben, '12A' sind zwölf A.

  • Mit einem regulären Ausdruck geht es in einer Zeile: code.scan(/(\d+)(\D)/) findet jede Zahl mit dem Zeichen dahinter, als Paare von Strings, etwa [['6', 'W'], ['3', 'B'], ['4', 'W']].
  • 'W' * 6 ist 'WWWWWW', und to_i macht aus '6' eine Zahl.
  • Mit einem regulären Ausdruck geht es kurz: Nach import re findet re.findall(r'(\d+)(\D)', code) jede Zahl mit dem Zeichen dahinter, als Paare von Strings, etwa [('6', 'W'), ('3', 'B'), ('4', 'W')].
  • 'W' * 6 ist 'WWWWWW', int('6') macht aus '6' eine Zahl, und ''.join(parts) hängt eine Liste von Strings aneinander.
Aufgabe 3Zurück zum Text

Schreib unrle.

Tipp

code.scan(/(\d+)(\D)/) liefert Paare wie ['6', 'W']. 'W' * 6 ist 'WWWWWW', '6'.to_i ist 6, und join hängt alles aneinander.

Aufgabe 3Zurück zum Text

Schreib unrle.

Tipp

re.findall(r'(\d+)(\D)', code) liefert Paare wie ('6', 'W'). 'W' * 6 ist 'WWWWWW', int('6') ist 6, und ''.join(...) hängt alles aneinander. Für re brauchst du oben import re.

4. Wenn Ziffern im Text stehen

Vermutung 4Ziffern im Text

rle('2021') ergibt '12101211'. Was macht unrle daraus?

Ziffern im Text

Als Text geht es nur, solange der Text selbst keine Ziffern enthält: Sonst weiß niemand, welche Ziffern Längen sind und welche zum Text gehören. Echte Programme schreiben deshalb immer genau zwei Bytes für jeden Lauf, eins mit der Länge und eins mit dem Byte, das sich wiederholt. Dann ist klar, was was ist, egal was in der Datei steht. Weil ein Byte höchstens 255 sein kann, wird ein längerer Lauf geteilt: 300-mal A sind [255, 65, 45, 65].

Schreib rle_bytes, das ein Array von Zahlen zurückgibt, und unrle_bytes, das daraus wieder den String macht.

  • data.each_byte liefert die Bytes als Zahlen.
  • bytes.each_slice(2) { |count, byte| … } geht das Array paarweise durch.
  • [65] * 3 ist [65, 65, 65], und [65, 65, 65].pack('C*') macht daraus den String 'AAA'.

Schreib rle_bytes, das eine Liste von Zahlen zurückgibt, und unrle_bytes, das daraus wieder Bytes macht.

  • b'AAAB' sind Bytes, keine Zeichen, und for byte in data: liefert sie als Zahlen: 65, 65, 65, 66.
  • zip(numbers[0::2], numbers[1::2]) geht eine Liste paarweise durch: die Zahlen an geraden und an ungeraden Stellen.
  • [65] * 3 ist [65, 65, 65], und bytes([65, 65, 65]) macht daraus b'AAA'.
Aufgabe 5Lauflängen als Bytes

Schreib rle_bytes und unrle_bytes. Ein Lauf ist immer zwei Zahlen, die Länge von 1 bis 255 und das Byte; längere Läufe werden geteilt.

Tipp

data.each_byte liefert Zahlen, bytes.each_slice(2) { |count, byte| … } geht ein Array paarweise durch, [byte] * count wiederholt, und pack('C*') macht aus Zahlen wieder einen String.

Aufgabe 5Lauflängen als Bytes

Schreib rle_bytes und unrle_bytes. Ein Lauf ist immer zwei Zahlen, die Länge von 1 bis 255 und das Byte; längere Läufe werden geteilt.

Tipp

for byte in data liefert Zahlen, zip(numbers[0::2], numbers[1::2]) geht eine Liste paarweise durch, [byte] * count wiederholt, und bytes(numbers) macht aus Zahlen wieder Bytes.

5. Wie gut ist das?

Lade drei Dateien zum Ausprobieren in den Ordner: die Diskette, achtfach vergrößert, als rohe Pixel (128 × 128 Bytes, jedes die Nummer einer Farbe), die Spezifikation von GIF als Text, und ein Foto als JPEG.

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

File.binread('diskette.raw') liest eine Datei Byte für Byte.

open('diskette.raw', 'rb').read() liest eine Datei als Bytes; das b in 'rb' ist wichtig, sonst bekommst du Text.

Lass dein Programm für jede ausgeben, wie groß sie ist und wie viele Zahlen rle_bytes daraus macht, und prüf dabei, ob unrle_bytes wieder genau die Datei ergibt.

Aufgabe 6Die Diskette

Wie viele Zahlen macht rle_bytes aus diskette.raw mit seinen 16.384 Bytes?

Tipp

rle_bytes(File.binread('diskette.raw')).size

Aufgabe 6Die Diskette

Wie viele Zahlen macht rle_bytes aus diskette.raw mit seinen 16.384 Bytes?

Tipp

len(rle_bytes(open('diskette.raw', 'rb').read()))

Vermutung 7Ein Text

Was passiert mit spec-gif89a.txt (83.003 Bytes), wenn du rle_bytes darauf anwendest? Rate erst, dann probier es aus.

Ein Text
Datei Bytes mit RLE
diskette.raw 16.384 dein Ergebnis
spec-gif89a.txt 83.003 125.796
44-baseline.jpg 143.206 284.916

In einem Text sind Läufe selten: Außer Leerzeichen und Linien aus - und = kommt kaum ein Zeichen zweimal hintereinander, und jedes einzelne kostet jetzt zwei Bytes. Im JPEG ist fast jedes Byte ein Lauf der Länge 1.

6. RLE in Fax und JPEG

Ein Faxgerät tastet eine Seite Zeile für Zeile ab, jede Zeile 1728 Pixel breit, und jedes Pixel ist weiß oder schwarz. Eine Zeile Text besteht dann aus langen weißen Läufen mit kurzen schwarzen dazwischen. Das Fax schickt nur die Längen der Läufe, abwechselnd weiß und schwarz, sodass es die Farbe gar nicht mitschicken muss, und codiert diese Längen mit festen Huffman-Codes: Häufige Längen bekommen kurze Codes.

JPEG macht etwas Ähnliches mit den Nullen. Nach der Quantisierung sind die meisten der 64 Zahlen eines Blocks 0. Jedes Symbol in der AC-Tabelle sagt deshalb zweierlei: wie viele Nullen vor der nächsten Zahl stehen, also einen Lauf, und wie viele Bits diese Zahl hat. Wie das genau aussieht, steht in Eine JPEG-Datei decodieren.

7. Weiter geht's

  • PackBits: Damit Dateien ohne Läufe nicht doppelt so groß werden, kennt das Verfahren PackBits (von Apple, auch in TIFF) zwei Sorten von Stücken. Ein Byte n von 0 bis 127 heißt: Die nächsten n + 1 Bytes kommen so, wie sie sind. Ein Byte von 129 bis 255 heißt: Das nächste Byte kommt 257 − n-mal. Schreib packbits und unpackbits und vergleich mit rle_bytes.
  • Spalten statt Zeilen: Lies diskette.raw spaltenweise statt zeilenweise. Wird es kleiner oder größer? Warum?
  • Huffman für die Längen: Zähl, welche Längen in diskette.raw vorkommen, und überleg, wie viele Bits sie mit Huffman bräuchten.