0

Steganographie mit PHP – Dateien in Bildern verstecken

Steganographie mit PHPIn diesem Artikel wollen wir uns mit dem Thema Steganographie beschäftigen und ein kleines Beispiel in PHP implementieren. Denn mit PHP lassen sich längst nicht “nur” Webseiten erstellen oder Formulare umsetzen, was mit Sicherheit auch jeder versierte PHP-Programmierer größerer Webagenturen bestätigen wird.

Neben Funktionen zur Textverarbeitung bringt PHP auch Methoden zur Bildbearbeitung sowie zur Manipulation auf Bit- und Byte-Ebene mit. Und eben diese wollen wir uns heute zunutze machen. Doch bevor es mit dem Coding losgeht, gibt es noch einen kurzen Einstieg in das Thema Steganographie.

Steganographie ist keine Kurzschrift

Wie der Titel bereits angekündigt hat, geht es heute um Steganographie, die Kunst Informationen oder Wissen in einem Trägermedium zu verstecken. Wikipedia definiert Steganographie wie folgt:

Die Steganographie (auch Steganografie) ist die Kunst oder Wissenschaft der verborgenen Speicherung oder Übermittlung von Informationen in einem Trägermedium (Container). Das Wort lässt sich auf die griechischen Bestandteile στεγανός steganós ‚bedeckt‘ und γράφειν gráphein ‚schreiben‘ zurückführen,[1] bedeutet also wörtlich „bedeckt schreiben“ bzw. „geheimes Schreiben“. Das modifizierte Medium wird als Steganogramm bezeichnet.
Quelle: https://de.wikipedia.org/wiki/Steganographie

Das mag auf den ersten Blick jetzt etwas abstrakt klingen, doch eigentlich ist Steganographie gar nicht so schwer zu verstehen. Nehmen wir an, wir sind ein verdeckter Ermittler und haben ein Foto von einem Verbrechen gemacht. Nun wollen wir dieses Foto unerkannt an unseren Feinden vorbeischleusen. Hierzu nehmen wir unser geheimes Foto und verstecken es innerhalb eines harmlosen Fotos. Zum Beispiel einer schönen Landschaftsaufnahme.

Nachdem wir unser geheimes Foto in die Landschaftsaufnahme (das Trägermedium) injiziert haben, wird daraus das sogenannte “Steganogramm”. Die Empfänger unseres Steganogramms nutzen wiederum ihr Wissen darüber, wie wir das Foto versteckt haben, und lesen es aus der Landschaftsaufnahme wieder aus. That’s it! So funktioniert Steganographie in seiner einfachsten Form.

In unserem Artikel wollen wir heute genau dieses Szenario nachbauen. Wir schreiben ein kleines PHP-Script, welches es uns ermöglicht, Dateien in einem Foto zu verstecken und somit unsere eigenen Steganogramme zu erstellen.

Ein Wort zum Ende der Einleitung – auch wenn es ähnlich klingt – Steganographie ist nicht Stenographie. Bei Stenographie handelt es sich um eine aus Symbolen bestehende Kurzschrift, die es ermöglicht, besonders schnell handschriftliche Notizen anzufertigen.

Der Programmablauf in der Theorie

Pixel - Funktionsweise von digitalen BildernDas Script, welches wir heute schreiben wollen, ermöglicht es Bilder in anderen Bildern zu verstecken. Wer den Artikel bis zum Ende durcharbeitet, sollte auch in der Lage sein, das Script so zu erweitern, dass beliebige Dateien in einem Foto versteckt werden können. Doch wie funktioniert dies technisch? Hierzu müssen wir zuerst betrachten, wie Bilder digital gespeichert werden.

Ein digitales Bild besteht aus einer Menge an Bildpunkten, den Pixeln. Jedes einzelne Pixel wiederum hat einen eigenen Farbwert. Diese Farbwerte/Farben setzen sich (in den meisten Fällen) aus den drei Grundfarben Rot, Grün und Blau zusammen.

Jede Grundfarbe wiederum wird mit einem Wert von 0-255 dargestellt und passt somit exakt in 1 Byte. Je höher der Wert der Grundfarbe, umso dominanter ist er in der Farbe des Pixels. Ein reines Rot besteht zum Beispiel aus: “Rot: 255, Grün: 0, Blau: 0”. Ein weißes Pixel wiederum würde als “Rot: 255, Grün: 255, Blau: 255” dargestellt werden.

Nachdem wir nun den grundlegenden Aufbau eines Bildes und die Funktionsweise eines Pixels kennengelernt haben, können wir uns dies für unser Vorhaben zunutze machen. Als Beispiel nehmen wir die Farbe Rot. Wie bereits beschrieben wird Rot als 255, 0, 0 (Rot, Grün, Blau) dargestellt. Ebenso haben wir festgestellt, dass der Wertebereich 0-255 entspricht und in 1 Byte passt. Somit könnten wir die Grundfarbwerte auch binär darstellen. (Wer Hilfe braucht, kann diesen Rechner hier nutzen.)

Farbunterschied 255 vs. 248In binärer Schreibweise würde Rot als 11111111, 00000000, 00000000 dargestellt werden. Eine Eigenschaft der binären Schreibweise ist, dass eine Änderung der Bits, je weiter rechts sie stattfindet, einen immer kleineren Einfluss auf den Gesamtwert hat. Ändert man z.B. die letzten 3 Bit im Rotwert von 111 auf 000, so ändert sich der Dezimalwert von 255 auf 248. Diese Änderung ist mit dem menschlichen Auge kaum wahrnehmbar, wie die nebenstehende Grafik zeigt, die Seite an Seite die beiden Farben zeigt.

Was lernen wir daraus? Wir können also mindestens die letzten 3 Bit eines Farbwertes anpassen, ohne dass dies groß auffällt. Überlegen wir nun weiter. Ein Pixel besteht aus drei Grundfarbwerten. Wenn wir je drei Bits anpassen, dann können wir in einem Pixel 9 Bit anpassen. Dies reicht also aus, um mehr als ein Byte in einem einzelnen Pixel zu codieren. Passen wir bei einer Farbe nur 2 anstelle von 3 Bits an, so kommen wir auf genau ein Byte.

Haben wir nun also ein Foto einer handelsüblichen Kamera, dass mit einer Auflösung von 12 Megapixel aufgenommen wurde, haben wir ein Bild mit 4048 × 3040 = 12.305.920 Pixeln. In jedes Pixel können wir 1 Byte ablegen. Bei 12.305.920 Pixeln können wir also auch 12.305.920 Byte abspeichern, was wiederum ~11,75 Mb entspricht.

Wie wir nun ein Byte der zu versteckenden Datei auf 3 Bytes (Rot-Byte, Grün-Byte, Blau-Byte) eines jeden Pixels aufteilen und warum wir in der Praxis nicht die volle Pixelanzahl in Speicher umwandeln können, folgt im nächsten Abschnitt, in welchem wir uns mit der Implementierung befassen.

Doch bevor es losgeht, noch ein Wort zum Trägermedium, dem Bild, in dem wir unser geheimes Foto verstecken wollen. Das Trägermedium muss zwingend in einem unkomprimierten oder verlustfrei komprimierten Dateiformat wie z.B. PNG, TIFF oder BMP vorliegen. Verlustbehaftete Formate funktionieren für dieses Steganographie-Verfahren nicht, denn wie der Name schon sagt, komprimieren solche Formate wie JPG mit Informationsverlusten. In der Praxis werden z.B. mehrere, farblich ähnliche Pixel zu Blöcken einer einzigen Farbe zusammenfassen. Hier würden uns also Informationen unserer geheimen Nachricht verloren gehen.

Doch nun genug der Theorie. Im folgenden Abschnitt beginnen wir, das nun kennengelernte Konzept in der Praxis umzusetzen.

Die Implementierung

Für die Implementierung werden wir das Script in kleinen Stücken aufbauen und diese einzeln besprechen. Wer den Überblick verliert, kann ans Ende des Beitrags scrollen. Dort befindet sich das komplette Script in einem Block.


//URLs des Trägermediums und der zu versteckenden Datei
$src_container = $_GET['img_container'];
$src_payload = $_GET['payload_file'];

//Bildgröße auslesen und maximale Bytegröße berechnen
$container_size = getimagesize($src_container);
$maxPayloadByte = $container_size[0]*$container_size[1]-4;

In den ersten beiden Code-Zeilen fragen wir die URLs des Trägermediums und der Payload ab. Die URL für das Trägermedium sollte auf eine verlustfrei komprimierte Bilddatei verweisen. (Wie z.B. eine PNG-Grafik.) Die URL für die Payload kann auf jede beliebige Datei verweisen. (Prinzipiell könnten die Dateien auch direkt von der Festplatte gelesen oder aus anderen Quellen bezogen werden. Der Einfachheit halber arbeiten wir jedoch mit URLs, die wir per GET-Parameter einlesen.)

In der dritten Zeile ermitteln wir mit der getimagesize-Funktion einige Werte zum Trägermedium. Besonders interessant sind hierbei die Maße (Breite und Höhe) in Pixeln des Trägermediums.

In der vierten Zeile berechnen wir nun die maximale Dateigröße in Byte, die wir in dem Trägermedium speichern können. Hierzu berechnen wir erst einmal die Anzahl der Pixel, in dem wir die Breite ($container_size[0]) mit der Höhe ($container_size[1]) multiplizieren. Da wir in jedem Pixel ein Byte ablegen können (siehe hierzu den Absatz “Programmablauf in der Theorie”), ergibt sich aus der Anzahl der Pixel also die maximale Speichermenge.

Von dieser Gesamtspeichermenge ziehen wir nun noch 4 Byte (respektive 32 Bit) ab. Diese Speichermenge “reservieren” wir uns, um dort die Dateigröße der zu versteckenden Datei abzulegen. Denn ist diese kleiner als der zur Verfügung stehende Speicher, müssen wir beim Auslesen ja wissen, wie viele Pixel einen Teil der geheimen Nachricht enthalten und wie viele nicht mehr Teil der Nachricht sind.

//Payload in Bytearray schreiben und Größe berechnen
$payloadByteArr = unpack("C*", file_get_contents($src_payload));
$payloadByteSize = count($payloadByteArr);

//Sicherheitsabfrage für Dateigrößen
if ($payloadByteSize > $maxPayloadByte)
{
die('Die Payload ist größer als der Cryptcontainer.');
}

Im nächsten Schritt nutzen wir die unpack-Funktion, um die zu versteckende Datei ($src_payload) in ein Array aus Bytes auszulesen/umzuwandeln. Danach lesen wir dann die Größe dieses Bytearrays ($payloadByteArr) mittels der count-Funktion aus.

Abschließend überprüfen wir, ob die maximale Dateigröße, die in dem Trägermedium versteckt werden kann ($maxPayloadByte), größer als die zu versteckende Datei ($payloadByteSize) ist. Wenn dem nicht so ist, also die geheime Datei nicht in das Trägermedium passt, brechen wir das Script mit einer Fehlermeldung und dem die-Befehl ab.

Ist das Trägermedium ausreichend groß, bereiten wir die Codierung vor. Hierzu legen wir noch ein paar Hilfsvariablen an.


//Trägermedium in Datei lesen und als Bild "öffnen"
$container = file_get_contents($src_container);
$img = imagecreatefromstring($container);
if (!$img) echo "error";

//Payload-Größe in Bytearray umschreiben
$payloadByteSizeArr = array((($payloadByteSize >> 24) & 0xFF),
     (($payloadByteSize >> 16) & 0xFF),
     (($payloadByteSize >> 8) & 0xFF),
     ($payloadByteSize & 0xFF) );

Zuerst lesen wir die geheime Datei in die Variable $container, um im nächsten Schritt mittels der imagecreatefromstring-Funktion ein Bild-Objekt ($img) daraus zu erstellen. Dies ist notwendig, um später die Farbwerte der einzelnen Pixel auslesen und ändern zu können. Sollte es hierbei einen Fehler geben, quittieren wir dies mit der Ausgabe eines Strings mit dem Wert “error”.

Als Nächstes nehmen wir uns die Größenangabe der Payload (=geheime Datei) vor. Diese müssen wir (zusammen mit den geheimen Daten selbst) mit in dem Trägermedium verstecken, um zu wissen, wie viele Pixel mit geheimen Daten versehen sind. Da die Größenangabe jedoch als 32-Bit Integer (Ganzzahl) in der Variable $payloadByteSize steht, wir jedoch in jedem Pixel nur 1 Byte unterbringen können, schieben wir je 8-Bit (=1 Byte) mittels des Bitshiftoperators (>>) und einer Bitmaske (&0xFF) aus dem Integer in ein Byte und legen dieses in dem Array $payloadByteSizeArr ab.

Wer diesem Abschnitt nicht ganz folgen konnte, liest sich am besten noch mal den Einstieg zum Thema Bit-Operatoren durch. Alternativ könnt ihr auch gerne einen Kommentar unter diesen Artikel mit eurer Frage posten. Ich werde dann mein Bestes geben, die Unklarheiten zu beseitigen.

Nun sind alle Vorarbeiten abgeschlossen. Wir haben Bildgrößen ermittelt, die Speichergrößen errechnet und geprüft und die Daten vorbereitet. Kommen wir also zum Codieren der geheimen Nachricht.

Um die Pixel des Trägermediums ($img) einzeln anzusprechen, beginnen wir mit zwei ineinander verschachtelten Schleifen.


//Für jeden Pixel auf der Horizontalen
for($x=0;$x<$container_size[0];$x++)
{
   //Für jeden Pixel auf der Vertikalen
   for($y=0;$y<$container_size[1];$y++)
   {
       //Die ersten 4 Pixel (=Byte) anders behandeln
      if ($y < 4 && $x == 0)
      {
         //Codeblock A
      }
      else
      {
         //Wenn Payload noch nicht vollständig versteckt
         if ((($x*$container_size[1])+$y-3) <= $payloadByteSize)
         {
            //Codeblock B
         }
      }
   }
}

Innerhalb der beiden Schleifen machen wir dann noch eine if-else-Abfrage, um zu ermitteln, ob es sich bei dem aktuellen x-y-Wert um einen der ersten 4 Pixel handelt. Dies machen wir, da wir die ersten vier Pixel nicht mit den Daten der geheimen Nachricht ($payloadByteArr), sondern mit den Größeninformationen der geheimen Nachricht ($payloadByteSizeArr) versehen möchten.

Im else-Block, also wenn es sich nicht um einen der ersten vier Pixel handelt, machen wir noch eine weitere if-Abfrage, die überprüft, ob das aktuelle Pixel, kleiner ist als die Dateigröße der zu versteckenden Datei. Hiermit stellen wir sicher, dass wir nur solange Pixel im Trägermedium manipulieren, wie auch Bytes der zu versteckenden Datei vorhanden sind.

Die eigentliche Manipulation des Bildes findet an den Stellen //Codeblock A und //Codeblock B statt, welche ich in oben stehenden Code-Snippet ausgespart habe, um die Schleifen übersichtlich zu halten. Mit den beiden Codeblöcken wollen wir uns aber nun beschäftigen. Beginnen wir mir Codeblock A…

//Payload größe codieren
$pixel=imagecolorat($img, $x, $y); 
$payloadSubBlock1 = ($payloadByteSizeArr[$y] & 0xE0) >> 5;
$payloadSubBlock2 = ($payloadByteSizeArr[$y] & 0x1C) >> 2;
$payloadSubBlock3 = ($payloadByteSizeArr[$y] & 0x3);
$payloadBlock = $payloadSubBlock1 << 16 | $payloadSubBlock2 << 8 | $payloadSubBlock3;
$pixel = ($pixel & 0xF8F8FC) | $payloadBlock;
imagesetpixel($img, $x, $y, $pixel);

In der ersten Zeile ermitteln wir mittels der imagecolorat-Funktion die Farbe des Pixels im Trägermedium ($img) an der Position x=$x und y=$y. Den Farbwert legen wir in der Variable $pixel ab. Hierbei gibt die imagecolorat-Funktion den Farbwert als 24-Bit Integer zurück. Je 8 der 24 Bit entsprechen einem der drei Farbkanäle Rot, Blau und Grün.

In den folgenden drei Zeilen nehmen wir uns ein Byte des $payloadByteSizeArr-Arrays, welches die Länge der zu speichernden Payload angibt. Dieses Byte zerlegen wir nun in drei Blöcke ($payloadSubBlock1..3). Hierzu nutzen wir sowohl Bit-Masken und den &-Operator als auch Bit-Shifting.

In der nächsten Zeile schieben wir unsere drei Teilblöcke der zu versteckenden Information in einen 24-Bit Integer, um diesen dann in der folgenden Zeile mit dem 24-Bit Integer aus $pixel zu verschmelzen können. In der letzten Zeile des Codeblocks wird die manipulierte Farbe dann mittels der imagesetpixel-Funktion wieder in das Trägermedium zurückgeschrieben.

Da diese Zeilen etwas verwirrend sein können, habe ich folgendes Beispiel für den “Codeblock A” verfasst, das mittels Testdaten den Vorgang noch einmal aufschlüsselt.


//Der Pixel an der Stelle x, y hat die Farbe Grün-Gelb (R=173,G=255,B=47)
$pixel=imagecolorat($img, $x, $y); 
//in Pixel steht nun: 10101101 11111111 00101111

//Das zu codierende Byte lautet:
//$payloadByteSizeArr[$y] -> 10111010
//Die Maske lautet:  0xE0 -> 11100000
$payloadSubBlock1 = ($payloadByteSizeArr[$y] & 0xE0);
//Durch & steht nun in $payloadSubBlock1: 10100000
$payloadSubBlock1 = $payloadSubBlock1 >> 5;
//Durch >> 5 steht in $payloadSubBlock1: 00000101

//0x1C -> 00011100
$payloadSubBlock2 = ($payloadByteSizeArr[$y] & 0x1C);
//Durch & steht nun in $payLoadSubBlock2: 00011000
$payloadSubBlock2 = $payloadSubBlock2 >> 2;
//Durch >> 2 steht in $payloadSubBlock2: 00000110

//0x3 -> 00000011
$payloadSubBlock3 = ($payloadByteSizeArr[$y] & 0x3);
//Durch & steht nun in $payloadSubBlock3: 00000010

//$payloadSubBlock1 << 16 entspricht: 00000101 00000000 00000000
//$payloadSubBlock2 << 8 entspricht:  00000000 00000110 00000000
//$payloadSubBlock3 entspricht:       00000000 00000000 00000010
$payloadBlock = $payloadSubBlock1 << 16 | $payloadSubBlock2 << 8 | $payloadSubBlock3;
//Nach Verknüpfung mit |-Operator: 00000101 00000110 00000010

//$pixel ist:   10101101 11111111 00101111
//0xF8F8FC ist: 11111000 11111000 11111100
//($pixel & 0xF8F8FC) ist: 10101000 11111000 00101100
//Maske löscht je 3 bzw. 2 unrelevanteste Bits des jeweiligen Farbkanal
$pixel = ($pixel & 0xF8F8FC) | $payloadBlock;
//Durch |-Operator werden gecleante Pixel mit $payloadBlock verbunden
//$pixel ist nun: 10101101 11111110 00101110

imagesetpixel($img, $x, $y, $pixel);
//neue Pixelfarbe wurde in Trägermedium gesetzt

Das ist auch schon der ganze Zauber. Kommen wir nun zu “Codeblock B”…


//Payload codieren
$pixel=imagecolorat($img, $x, $y);
$payloadSubBlock1 = ($payloadByteArr[($x*$container_size[1])+$y-3] & 0xE0) >> 5;
$payloadSubBlock2 = ($payloadByteArr[($x*$container_size[1])+$y-3] & 0x1C) >> 2;
$payloadSubBlock3 = ($payloadByteArr[($x*$container_size[1])+$y-3] & 0x3);
$payloadBlock = $payloadSubBlock1 << 16 | $payloadSubBlock2 << 8 | $payloadSubBlock3;
$pixel = ($pixel & 0xF8F8FC) | $payloadBlock;
imagesetpixel($img, $x, $y, $pixel);

Was fällt auf? CodeblockB gleicht CodeblockA bis auf ein kleines Detail. Die Funktionsweise ist bis auf den einen Unterschied komplett gleich, sodass ich an dieser Stelle auf eine weitere Erklärung verzichte.

Der einzige Unterschied liegt darin, wie der Index berechnet wird, an dem das zu versteckende Byte aus dem $payloadByteArr-Array entnommen wird.

In CodeblockA hatten wir an dieser Stelle die Variable $y genommen. Dies geht für die Payload nicht mehr, da wir zu den Spalten noch die Zeilen $x mit einbeziehen müssen. Schließlich wird $y für jede Spalte (jeden Schleifendurchlauf) ja wieder 0, weshalb wir $x in die Index-Berechnung hinzunehmen. Abschließend reduzieren wir den Index noch um 3 Positionen, da der Pixelindex ($x, $y) ja schon durch CodeblockA fortgeschritten ist, wir jedoch für das Auslesen aus $payloadByteArr an dessen Index 0 beginnen wollen.

Das komplette Script

Für die Erklärung war es sicherlich hilfreich das Script in einzelnen Blöcken bzw. zeilenweise zu analysieren. Wer nun jedoch den Überblick verloren hat, der möge folgenden Codeblock anschauen, welcher das komplette Script in einem Block anzeigt.

<?php //URLs des Trägermediums und der zu versteckenden Datei $src_container = $_GET['img_container']; $src_payload = $_GET['payload_file']; //Bildgröße auslesen und maximale Bytegröße berechnen $container_size = getimagesize($src_container); $maxPayloadByte = $container_size[0]*$container_size[1]-4; //Payload in Bytearray schreiben und Größe berechnen $payloadByteArr = unpack("C*", file_get_contents($src_payload)); $payloadByteSize = count($payloadByteArr); //Sicherheitsabfrage für Dateigrößen if ($payloadByteSize > $maxPayloadByte)
{
	die('Die Payload ist gr&ouml;&szlig;er als der Cryptcontainer.');
}

//Trägermedium in Datei lesen und als Bild "öffnen"
$container = file_get_contents($src_container);
$img = imagecreatefromstring($container);
if (!$img) echo "error"; 

//Payload-Größe in Bytearray umschreiben
$payloadByteSizeArr = array((($payloadByteSize >> 24) & 0xFF), 
                             (($payloadByteSize >> 16) & 0xFF), 
                             (($payloadByteSize >> 8) & 0xFF), 
                             ($payloadByteSize & 0xFF) );


//Für jeden Pixel auf der Horizontalen
for($x=0;$x<$container_size[0];$x++)
{
    //Für jeden Pixel auf der Vertikalen
    for($y=0;$y<$container_size[1];$y++)
    { 	
        //Die ersten 4 Pixel (=Byte) anders behandeln
        if ($y < 4 && $x == 0) { //Payload größe codieren $pixel=imagecolorat($img, $x, $y); $payloadSubBlock1 = ($payloadByteSizeArr[$y] & 0xE0) >> 5;
    		$payloadSubBlock2 = ($payloadByteSizeArr[$y] & 0x1C) >> 2;
    		$payloadSubBlock3 = ($payloadByteSizeArr[$y] & 0x3);
    		$payloadBlock = $payloadSubBlock1 << 16 | $payloadSubBlock2 << 8 | $payloadSubBlock3;
    		$pixel = ($pixel & 0xF8F8FC) | $payloadBlock;
    		imagesetpixel($img, $x, $y, $pixel);    	
    	}
    	else 
    	{
            //Wenn Payload noch nicht vollständig versteckt
    		if ((($x*$container_size[1])+$y-3) <= $payloadByteSize) { //Payload codieren $pixel=imagecolorat($img, $x, $y); $payloadSubBlock1 = ($payloadByteArr[($x*$container_size[1])+$y-3] & 0xE0) >> 5;
	    		$payloadSubBlock2 = ($payloadByteArr[($x*$container_size[1])+$y-3] & 0x1C) >> 2;
	    		$payloadSubBlock3 = ($payloadByteArr[($x*$container_size[1])+$y-3] & 0x3);
	    		$payloadBlock = $payloadSubBlock1 << 16 | $payloadSubBlock2 << 8 | $payloadSubBlock3; $pixel = ($pixel & 0xF8F8FC) | $payloadBlock; imagesetpixel($img, $x, $y, $pixel); } } } } header('Content-type: '.$container_size['mime']); imagepng($img); imagedestroy($img); ?>

Kommen wir zum Abschluss des Artikels…

Fazit

Erst mal: “Hut ab und herzlichen Glückwunsch.” Wer es bis zu dieser Stelle des Artikels geschafft hat, hat nicht nur ein funktionierendes Steganographie-Script geschrieben, sondern sicherlich auch etwas gelernt und die “Grauen Zellen” wieder einmal etwas in Schwung gebracht.

Das Script an und für sich ist eigentlich weder lang, noch sehr komplex. Dennoch fasziniert es (zumindest mich), was mit so wenig Zeilen Code möglich ist.

Dem aufmerksamen Leser mag aufgefallen sein, dass wir nur das Script zum Codieren, aber keines zum decodieren geschrieben haben. Dies hat jedoch auch einen Grund. Wer das Thema wirklich verstanden hat, der kann auch das “Umkehr-Script” schreiben. Und wer es nicht verstanden hat, darf mir gerne Kommentare schreiben, solange bis er es verstanden hat. Wer nur auf hastiges Copy’n’Paste aus ist, der guckt heute einfach mal in die Röhre.

Für die ganz Fleißigen noch ein paar Ideen zur Verbesserung des Scripts:

  • Analog zu der Payloadgröße könnte man noch den Dateinamen der Payload mit eincodieren. Somit müsste der Empfänger nicht zwingend das Dateiformat kennen, um die decodierte Nachricht anzuzeigen.
  • Wenn die Payload kleiner als das Trägermedium ist, könnte man entweder nur jedes x-te Pixel mit Payload-Daten versehen oder einfach nur das jeweils letzte Bit eines Farbkanals ändern. So ließe sich die Payload noch unauffälliger verstecken.
  • Die Payload-Bytes könnten vor dem Eincodieren noch verschlüsselt oder zumindest pseudo-zufällig angeordnet werden. (Dies macht es noch schwerer, durch Bildanalysen zu erkennen, ob in dem Trägermedium einer geheimen Datei versehen ist.)

Zum Abschluss noch eine kleine Motivation. Im Artikelbild (erstes Bild, oben links im Artikel) habe ich eine geheime Nachricht (.jpg-Datei) codiert. Wer ein Decoder-Script schreibt und mir als Erster den Inhalt der Nachricht in den Kommentaren nennt, bekommt eine Powerbank von mir geschenkt. (Davon habe ich aus einem anderen Projekt noch ein paar hier rumliegen…)

0

Analyse eines Javascript Poker Hand Evaluators

Poker in JS, Bits und BytesNachdem wir uns dem Thema Poker schon in einigen C#-Tutorials (siehe 1, 2, 3, 4) angenommen haben, wollen wir heute den Blick in Richtung Javascript lenken. Und wenn ich sage Javascript, dann meine ich das auch so. Bibliotheken wie jQuery (die dieser Tage viel zu oft fälschlicherweise mit Javascript gleichgesetzt werden) lassen wir heute aus dem Spiel. Doch was genau wollen wir heute eigentlich erstellen?

Im heutigen Artikel wollen wir noch einmal das Thema “Hand Evaluator” beackern. Also jenes Thema, welches wir an dieser Stelle schon in C# umgesetzt haben. Für alle, die den letzten Artikel nicht gelesen haben oder mit C# nichts anfangen können, noch einmal schnell die Zusammenfassung, was ein Poker Hand Evaluator ist und wozu er genutzt wird.

Ein Evaluator ist ein Programm, dass Eingabewerte evaluiert – also Eingabewerte untersucht und bewertet. Ein Poker Hand Evaluator untersucht also eine Poker Hand und bewertet diese. In der Praxis sieht das wie folgt aus. Man übergibt dem Hand Evaluator eine den Handstapel, bestehend aus 5 Spielkarten und der Evaluator gibt dann an, welches die beste Hand (z.B. Paar, Flush oder Full-House) ist, die aus diesen Karten hervorgeht.

Verwendung findet ein solcher Hand Evaluator praktisch in jeder Poker-Implementierung. Sei es Online-Poker, ein Poker-Trainer oder eine Poker-App. Er ist integraler Bestandteil bei der Abbildung der Spiellogik.

In dem heutigen Artikel, wollen wir eine etwas “komplexere” Umsetzung in Javascript eines Hand Evaluators analysieren. Konkret geht es um den Evaluator von subskybox, welchen ihr hier findet. Da ich der Meinung bin, dass der Code absolut genial, jedoch recht schwer zu verstehen ist, wollen wir heute einmal schauen, wie er funktioniert.

Der Einstieg

Die Originalfunktion von subskyboy besteht aus nur 4 Zeilen Code zur Ermittlung der Hand und einer Zeile zur Ausgabe. Das an und für sich ist schon ein Meisterwerk. Bedenke man wie viel Zeilen Code wir für unsere C# Implementierung gebraucht haben, die ich am Anfang dieses Artikel verlinkt habe.

Der Code von subskybox sieht wie folgt aus:


hands=["4 of a Kind", "Straight Flush", "Straight", "Flush", "High Card",
"1 Pair", "2 Pair", "Royal Flush", "3 of a Kind", "Full House" ];
var A=14, K=13, Q=12, J=11, _ = { "♠":1, "♣":2, "♥":4, "♦":8 };

function rankPokerHand(cs,ss) {
var v, i, o, s = 1<<cs[0]|1<<cs[1]|1<<cs[2]|1<<cs[3]|1<<cs[4];
for (i=-1, v=o=0; i<5; i++, o=Math.pow(2,cs[i]*4)) {v += o*((v/o&15)+1);}
v = v % 15 - ((s/(s&-s) == 31) || (s == 0x403c) ? 3 : 1);
v -= (ss[0] == (ss[1]|ss[2]|ss[3]|ss[4])) * ((s == 0x7c00) ? -5 : 1);
document.write("Hand: " + hands[v] + (s == 0x403c?" (Ace low)":"")+"</br>");
}

Da der originale Code schon fast wie C anmutet und extrem kompakt ist, werden wir ihn an vielen Stellen umschreiben, um die Funktionsweisen dahinter zu verstehen. Sicher, subskybox’ anliegen war es, einen möglichst kurzen Code zu schreiben, doch für die Analyse kommen wir nicht umhin, den Code zu erweitern.

Weiter nutzt der originale Code eine Mischung aus Bit- und arithmetischen Funktion, was der Tatsache geschuldet ist, dass Bit-Operationen nur auf 32-Bit-Typen anwendbar sind, wir jedoch 52-Bit benötigen, um den Hand-Evaluator abbilden zu können.

Um die Kartenwerte- und Farben zu speichern, werden zwei verschiedene Formate genutzt. Beide legen die Informationen auf Bit-Ebene ab.

Das erste Format speichert welcher Kartenwert wie oft in der Hand vorkommt. Hierzu werden die 52-Bit verwendet, wobei 4 Bit je Kartenwert verwendet werden, um die einzelnen Farben/Anzahlen jedes Kartenwertes als Bit-Flags abzubilden. (13 Werte * 4 Farben = 52 Kombinationen/Bit.)

Mit dieser Darstellung können wir den Großteil der möglichen Hände wie z.B. Paare oder Drillinge erkennen. Da wir jedoch mit 52-Bit arbeiten, kommen wir in diesem Format auch nicht mehr mit den reinen Bit-Operatoren aus, sondern müssen zusätzlich noch mit arithmetischen Funktionen arbeiten.

Das zweite Format beachtet keine Dopplungen, sondern lediglich, welche Kartenwerte in der Hand vorhanden sind. Hierzu wird ein 13-bittige Darstellung genutzt, wobei jede Stelle einem Kartenwert (2, 3, 4, …, König, Ass) entspricht. Befindet sich ein Wert in der Hand, wird das Bit an entsprechender Position der 13-Bit-Folge auf 1 gesetzt. Da wir mit 13 Bit auskommen, können wir hier auch mit den normalen Bit-Operatoren arbeiten.

Der Code beginnt mit der Deklaration einiger Konstanten. So werden ein Array für die möglichen Hände (wobei die Reihenfolge eine wichtige Rolle spielt – dazu später mehr), ein paar Hilfsvariablen für die Kartenwerte (das Ass bekommt den Wert 14!) und ein weitere Array für die Kartensymbole angelegt.

Die original Funktion besitzt zwei Parameter – cs für die Kartenwerte der Hand und ss für die Farben der Karten in der Hand. Der Lesbarkeit halber sind die Variablen im folgenden als cardRanks und cardSuits benannt.

Im ersten Schritt erstellen wir ein BitArray, um damit später Strassen erkennen zu können. Dieses Array wird im zweiten, der beiden eben vorgestellten Formate gespeichert.

Erstellen des cardValues-BitArray

Für die Erstellung des cardValues-Array benötigen wir ausschließlich Bit-Operationen. Genauer gesagt den sogenannten “Bit-Shift-Operator” sowie den OR-Operator.

Der Bit-Shift-Operator wird in Javascript (und fast allen anderen Programmiersprachen auch) als << und >> dargestellt und verschiebt eine Gruppe von Bits um X Positionen nach rechts oder links.

Hierzu folgende Beispiel: 3 << 1

3 == 0000 0011 //Darstellung von 3 als Binärcode
0000 0011 << 1 == 0000 0110 //Verschiebung der Bits
0000 0110 == 6 //Ergebnis des Bit-Shift

Um nun einen Kartenwert zu Speichern, verschieben wir den Wert 1 um “Kartenwert” Positionen. Wollen wir also die 7 ablegen, würden wir folgenden Code nutzen.


0000 0001 << 7 = 1000 0000

Wollen wir im Anschluss die 4 ablegen, nutzen wir folgenden Code.


0000 0001 << 4 = 0001 0000

Um diese beiden Werte nun platzsparend zu speichern, kann man sie mit einander kombinieren. Dies geht mittels des OR-Operators.


(1000 0000 | 0001 0000) == 1001 0000

Führen wir dieses Beispiel nun fort und schreiben es für die 5 Karten der Pokerhand um, kommen wir auf die erste Zeile von subskybox’ Funktion:


var cardValuesField = 1<<cardRank[0] |1<<cardRank[1]|1<<cardRank[2]|1<<cardRank[3]|1<<cardRank[4];

Wird shiften also für jede Karte der Hand eine 1 um den Wert der Karte nach links und kombinieren alle Werte mittels OR-Operator zu einem einzigen BitArray. Obige Zeile entspricht ziemlich genau der ersten Zeile von subskybox’ Code, mit dem Unterschied, dass wir die Variablen etwas sprechender benannt haben. Im nächsten Schritt nehmen wir uns dem zweiten BitArray an.

Das cardCountBitField-BitArray

Nun kommen wir zum zweiten Array, welches eingangs bei der Format-Beschreibung als erstes Format beschrieben wurde.

Mittels dieses Arrays können wir nicht nur bestimmen, welche Kartenwerte in der Hand sind, sondern auch wie oft diese vorkommen, sodass wir Hände wie z.B. Pärchen oder Drillinge oder Full House erkennen können.

Wie bereits beschrieben, kommen wir nun nicht mehr mit den reinen Bit-Operatoren aus, sondern werden uns auch arithmetische Operatoren zu Nutze machen.

In der Originalfunktion handelt es sich hierbei um die folgende (zweite) Zeile.


for (i=-1, v=o=0; i<5; i++, o=Math.pow(2,cs[i]*4)) {v += o*((v/o&15)+1);}

Um diese Zeile zu verstehen, brechen wir den Code in mehrere Zeilen auf. Doch zuerst werfen wir einen Blick auf die verwendeten Variablen. Derer gibt es 3 an der Zahl:

  • i => Die Zählvariable der Schleife, welche genutzt wird, um die Position im cs (cardRank) Array anzugeben.
  • o => Gibt die aktuelle Nibble Position an. (Als Nibble bezeichnet man in der IT einen 4-Bit-Block, auch Halbbyte genannt.)
  • v => Gibt die Anzahl der jeweiligen Karte an. (Der Lesbarkeit halber, geben wir auch dieser Variable einen sprechenden Namen: cardCountBitfield)

Ersetzen wir die Variablennamen in der Originalzeile und fügen Zeilenumbrüche, erhalten wir folgendes, schon etwas besser zu lesendes, Ergebnis:


for (index=-1, cardCountBitField=nibblePosition=0; index<5; index++, nibblePosition=Math.pow(2,cardRank[index]*4)) {
cardCountBitField += nibblePosition*((cardCountBitField/nibblePosition&15)+1);
}

Leider ist das immer noch nicht gerade verständlich, weshalb wir die Funktion noch einmal wie folgt umschreiben:


for (i=0; i<5; i++) {
currCount = getCurrentCountForCardRank(cardCountBitField, cardRank[i]);
cardCountBitField = setValueForCardRank(cardCountBitField, cardRank[i], currCount + 1);
}

Nun sollte schon eher klar sein, was innerhalb der Schleife passiert. Für jeder der fünf Karten ermitteln wir im ersten Schritt die Anzahl (=currCount) des aktuellen Kartenwerts (=cardRank[i]).

Im nächsten Schritt (= zweite Zeile) erhöhen wir die Anzahl um 1 und schreiben das Ergebnis zurück in das BitArray cardCountBitField. Da cardCountBitField 52-Bit lang ist und für jeden Kartenwert 4-Bit vorsieht, können wir mit der Variable in jedem Schleifendurchlauf arbeiten, ohne dass wir bereits ermittelte Werte überschreiben würden.

Was noch auffallen dürfte, ist, dass wir zwei Funktionen in den Code einbaut haben. Diese erhöhen die Lesbarkeit. Innerhalb der Funktionen wird jedoch derselbe Code ausgeführt, den auch subskybox genutzt hat. Die Erklärung der einzelnen Funktionen erfolgt in den nächsten Abschnitten dieses Artikels.

Erkennen der Nibble-Position

Bevor wir zu den beiden neuen Funktionen kommen, beschäftigen wir uns zuerst mit der Halbbyte-Position. In unserem Code ist dies die Variable nibblePosition. (Im Originalcode die Variable o.)

Der Originalcode sieht an dieser Stelle wie folgt aus:


o=Math.pow(2,cs[i]*4))

Jeder Kartenwert benötigt 4-Bit Speicher (1 Bit pro Farbe). Um nun zu ermitteln in welchem Halbbyte der Kartenwert steckt, müssen wir bei 0 beginnen und in Viererschritten durch die 52 Bits des cardCountBitField-Array gehen.

Da wir jedoch nicht mit klassischen Arrays arbeiten, müssen wir später beim Hochzählen 1 Bit an einer bestimmten Stelle setzen. Wie bereits in dem Abschnitt über Bit-Shifting erklärt, müssen wir also eine Zahl nutzen, die das Bit bereits an der richtigen Stelle gesetzt hat und diese per OR-Operator mit dem bestehenden BitArray verbinden. Dies ginge mit reinen Bit-Operatoren wie folgt (wobei value der Zahl entspricht, die wir per OR über das BitArray legen wollen und index der Position entspricht, an der wird das Bit setzen wollen):


var value = 1 << index

Den Wert (=value) könnten wir also wieder mit dem OR-Operator (=|) auf die bestehende Bit-Folge legen. Hierbei gibt es  jedoch einen Haken…

Da wir pro Kartenwert 4 Bits benötigen müssen wir dies natürlich berücksichtigen. So wird der index mit 4 multipliziert.


var value == 1 << (index * 4)

Bei 13 Kartenwerten und 4 Farben (bzw. dem Anzahlwert, der maximal 4 sein kann) kommen wir nun jedoch auf 52-Bit und können somit nicht mehr die Bit-Operationen wie den Bit-Shift nutzen, da diese nur auf 32-Bit Werten funktionieren. (Dieses Problem hatte ich eingangs schon bei der Erklärung der beiden Darstellungsformate angesprochen.)

Mit etwas Trickserei und der Funktionsweise von Binärcode können wir das Problem jedoch umgehen. Bedenkt man, dass man nur eine 1 auf dem Index und an allen anderen Positionen eine 0 haben möchte, so könnte man die gesuchte Position der 1 auch wie folgt darstellen: value = 2^(index*4)

In Javascript-Code abgebildet und in eine Funktion verpackt, ergibt das folgenden Code.


function getPositionForCardRank(cardRank) {
return Math.pow(2,cardRank*4);
}

Mit dem Wissen darüber, wie wir die richtige Index-Position ermitteln können, können wir uns nun mit der ersten, der beiden neuen Funktionen (getCurrentCountForCardRank) beschäftigen.

Auslesen der aktuellen (Anzahl-)Wertes

Um die Anzahl des aktuellen Kartenwerts auszulesen, machen wir noch einmal einen kleinen gedanklichen Exkurs in den Fall, wo wir Bit-Shifting nutzen können. Was wir erreichen möchten, ist die 4 Bits auszulesen, die dem Kartenwert entsprechen.

Wir wissen, dass wir die Bits an der Position index*4 finden werden. Mit Bit-Shifting könnten wir nun um index*4 nach rechts shiften, um die gesuchten 4 Bits zu erhalten.


var value = cardRank >> (index*4)

Nachdem wir nun alle rechtsseitigen Bits vom gesuchten Wert entfernt haben, müssten wir noch die Bits links von unserem gesuchten Halbbyte entfernen. Hierzu kann man den AND-Operator nutzen. Im Gegensatz zum OR-Operator, den wir bereits genutzt haben, gibt der AND-Operator nur eine 1, wenn beide, übereinander gelegten Werte 1 (=true) sind.

Dies können wir uns zu Nutze machen, indem wir eine Maske (=Bitmask) anlegen, die auf allen Positionen, bis auf unseren 4 gesuchten Bit, eine 0 hat.

0000 0000 0000 1111 //Maske = dezimal 15
&0001 0000 0001 0111 //cardRanks nach Bit-Shift
----------------------------
0000 0000 0000 0111

Durch den AND-Operator haben wir nun alle Stellen, außer unseren gesuchten 4 Bits, genullt. Da wir jedoch auf unseren 52-bittigen cardRanks-Wert keine Bit-Operation ausführen können, beenden wir nun diesen gedanklichen Exkurs und schauen uns die Praxislösung an.

Hier machen wir wieder von den arithmetischen Operatoren Gebrauch. Für den Links-Shift, wie wir ihn in der Indexermittlung genutzt haben, haben wir den Exponenten zur Basis zwei multipliziert. (Zur Erinnerung: value=2^index*4)

Ähnlichen können wir nun auch den Rechts-Shift machen. Hierzu nutzen wir lediglich die Division anstelle einer Multiplikation im Exponenten.

Um zum Beispiel ein Bit um 3 Positionen nach rechts zu schieben (0001 0000 => 0000 0010), eignet sich folgender Term: 2^(5-shiftCount) = 2^(5-3) = 2^5/2^3 = 2^2

Nun können wir also die gesuchten 4 Bits auch ohne Shift-Operator ans Ende verschieben. Jetzt, wo wir Sie an der richtigen Position schieben können, können wir auch mittels des AND-Operators wieder die linksseitigen Bits nullen.

Mit dem oben stehenden Wissen, können wir nun die Funktion zum Auslesen des Anzahl-Wertes schreiben. Hierzu nutzen wir zusätzlich die Funktion zur Ermittlung des Indexes (getPositionForCardRank) aus dem vorherigen Abschnitt dieses Artikels.


function getCurrentCountForCardRank(array, cardRank) {
var position = getPositionForCardRank(cardRank);
return array/position&15;
}

Zu diesem Zeitpunkt können wir also die aktuelle Anzahl der Karten eines Wertes auslesen und haben die erste der beiden Funktionen der zweiten Zeile fertig.

Im nächsten Schritt bilden wir nun die zweite Funktion, um den Kartenzähler zu erhöhen.

Erhöhen des aktuellen (Anzahl-)Wertes

Werfen wir zuerst wieder einen Blick auf den Originalcode. Für das setzen des Zählerwerts ist im Original folgender Teil der zweiten Zeile zuständig:

v += o*((v/o&15)+1);

Unser Ziel ist es den Zähler zu erhöhen. Haben wir zum Beispiel bereits zwei Karten des selben Wertes und bekommen eine Dritte, so würden wir im Normalfall einfach value = value+1  schreiben. Aber wie bereits mehrmals erwähnt, arbeiten wir ja mit Bits – und zwar 4 Bits für maximal 4 Karten. Wir wollen also ein Flag setzen.

Für vorangegangenes Beispiel hätten bei bei zwei Karten also 0011 und wollen daraus 0111 machen. Dies erledigen wir in zwei Schritten. Zuerst erhöhen wir den bestehenden Wert um 1. Aus 0011 wird also 0100. Wenn wir diesen Wert nun mit dem Ausgangswert addieren, erhalten wir den gesuchten Wert: 0011 + 0100 == 0111

Als Funktion bilden wir dies wie folgt ab:


function addValueToCardRank(array, cardRank, value) {
var position = getPositionForCardRank(cardRank);
return array + position * value;
}

Mit dieser Funktion können wir nun auch die zweite, neue Funktion komplettieren. Aus der zweiten Zeile des Originalcodes…

for (i=-1, v=o=0; i<5; i++, o=Math.pow(2,cs[i]*4)) {v += o*((v/o&15)+1);}

wurde nun dieser Code:


for (i=0; i<5; i++) {
currCount = getCurrentCountForCardRank(cardCountBitField, cardRank[i]);
currCount++;
cardCountBitField = addValueToCardRank(cardCountBitField, cardRank[i], currCount);
}

Zeit, einmal kurz “Halbzeit” zu rufen, sich selbst einen Belohnungs-Drink zu servieren und kurz zu feiern, denn wir haben nun die ersten beiden der ingesamt vier Zeilen Code der Originalfunktion analysiert.

Die verbleibenden beiden Zeilen beschäftigen sich mit der Erkennung/Evaluierung der gegebenen Pokerhand.

Pokerhände erkennen – die Königsdisziplin

Nachdem wir unsere Kartewerte und -anzahlen nun vorbereitet haben, können wir uns mit der Erkennung der Hände beschäftigen. In der Originalfunktion geschieht dies in der dritten Zeile, welche wie folgt aussieht:


v = v % 15 - ((s/(s&-s) == 31) || (s == 0x403c) ? 3 : 1);

Diese kurze Zeile Code übernimmt, wie schon die Zeilen zuvor, wieder mehrere Aufgaben. Sie erkennt fast alle möglichen Hände, bis auf “Straight”, “Straight Flush” und “Royal Flush”. Mit diesen Händen beschäftigen wir uns in einem späteren Abschnitt.

Beginnen wir mit dem Modulo-Operator (%). Normalerweise würden wir erwarten, den Restwert von v bei einer Division durch 15 zu erhalten. Da wir uns jedoch im Bit-Umfeld bewegen erreichen wir mit der Funktion, dass jeder der Kartenwerte, dargestellt durch 4 Bit, zusammengezählt wird. Der Wert 15 wird genutzt, da er dem Maximalwert eines 4-bittigen Kartenwerts (1111) entspricht.

An dieser Stelle kommt das hands-Array ins Spiel. Es zeigt sich, dass für jede mögliche Hand, die summierten Werte einzigartig (=unique) sind. Noch interessanter ist, dass die Reihenfolge der Karten auf der Hand dafür egal ist und die Summe nicht beeinflusst.

Das mag auf den ersten Blick nun verwirrend klingen, doch, wenn wir betrachten, wie die Karten zusammengezählt werden, wird schnell klar, warum dies so ist.

Nehmen wir als Beispiel ein Paar. Hierzu müssten wir zwei gleiche Karten und 3 unterschiedliche Karten haben. In unserer 4-bittigen Darstellung der Anzahlwerte entspräche dies zum Beispiel: 0011 + 0001 + 0001 + 0001 == 0110 == 6

Oben stehendes Beispiel macht deutlich, dass der Kartenwert für die Berechnung der Summe unrelevant ist. Egal ob zwei Asse oder zwei Siebenen – die binäre Darstellung der Anzahl wäre in jedem Fall 0011.

Bedingt durch diese Tatsache können wir einen Großteil der möglichen Hände mittels einem einfachen Array auslesen. Wichtig ist hiebei nur die Reihenfolge der Hände innerhalb des Arrays.


hands = ["4 of a Kind","","","","High Card","1 Pair","2 Pair","","3 of a Kind","Full House"];

Da Arrays Null-Index basiert sind, ziehen wir 1 von der errechneten Summe ab, um den passenden Wert im Array zu finden.

Hat man also zum Beispiel vier mal die 8 und eine 5 auf der Hand, dann wären die 4-bittigen Anzahlwerte mit den Flags 1111 und 0001. Bildet man nun die Summe, erhält man: 1111 + 0001 = 0001 0000 = 16. 16 % 15 ergibt wiederum 1. Zieht man nun noch den einen ab (wegen des 0-basierten Index des Arrays) kommt man auf 0. An Index 0 im Array steht wiederum “4 of a Kind” – also die korrekte Hand.

Als weiteres Beispiel nehmen wir ein Full House. Nehmen wir an, wir haben drei mal die 9 und zweimal die 7. Addiert man die 4-bittigen Anzahlwerte, erhält man: 0111 + 0011 = 0000 1010 = 10. Nun wieder den Modulo: 10 % 15 = 10. Einen für den 0-basierten Index abgezogen ergibt 9. Und an Index 9 im Array steht… natürlich Full House. Ich denke die Funktionsweise sollte bis hierhin klar sein.

Weiter fällt an obigem Array auf, dass mehrere Position des Arrays mit Leer-String (“”) gefüllt sind. Diese Positionen stehen für die Hände, die wir mit obiger Methode nicht abbilden können. Doch dazu sofort mehr…

Straights erkennen

Für die Erkennung der Straßen (=Straights) nutzen wir die andere Darstellung der Karten. (Jene, für die wir keine Anzahlwerte berechnet, sondern uns nur die Werte gemerkt haben.) Bei dieser anderen Darstellung hatten wir nur die Kartenwerte in einer 13 Bit langen Folge gespeichert. (Jede Bit-Position entsprach einem Wert: 2, 3, 4, …, König, Ass.)

Eine Straße ist definiert als eine Folge von 5 Karten. Was wir nun also suchen, ist eine Folge von fünf 1 in unserem 13 Bit langen Bitarray. Hierzu wollen wir alle rechtsseitigen Nullen entfernen. Übrig bliebe bei einer Straße dann nämlich folgende Bitfolge: 11111 welche der Dezimalzahl 31 entspricht. Nach dem Entfernen der Nullen könnten wir also einfach Abfrage machen, ob das Ergebnis = 31 ist und wenn dem so ist, würde eine Straße vorliegen.

Aber wie entfernen wir die Nullen am rechten Rand? Da die Anzahl unbekannt ist, können wir nicht um einen festen Wert shiften. Doch auch hierfür gibt es eine Lösung. (Welche übrigens auch als “Normalisierung eines Bitarrays” bezeichnet wird.)

Zuerst benötigen wir die erste 1 innerhalb des Bytearrays, um danach mittels Bit-Shift die Nullen zu eliminieren. Für die Ermittlung der ersten 1 machen wir uns das sogenannte Zweierkomplement zu nutze. Dieses wird eigentlich genutzt, um negative Zahlen im Binärsystem darzustellen. Gebildet wird es, indem man alle Bits negiert und danach 1 addiert.


0100 //7
1011 //negiert
1100 //+1 entspricht dem Zweierkomplement

Wenn man Ausgangswert und Zweierkomplement mit einem AND-Operator verbindet, erhält man die Position des ersten 1er Bit.


0100
&1100
--------
0100

Teilt man nun den Ausgangswert durch die Position des ersten Bits, so entspricht dies einem abtrennen der rechtsseitigen Nullen. Zur Verdeutlichung folgendes Beispiel:


0 0001 1111 0000 //13-bittige Darstellung; entspricht 496
1 1110 0001 0000 //Zweierkomplement
0 0000 0001 0000 //=Ausgang AND Zweierkomplement = Position

0 0001 1111 0000 //Ausgangswert (496)
/0 0000 0001 0000 //geteilt durch Position (16)
0 0000 0001 1111 //abgeschnitte Nullen (31)

Genau folgendes geschieht in der dritten Zeile des Originalcodes, wobei die Variable s den Ausgangswert (=cardValuesField) enthält.


s/(s&-s)

Zuerst wird s mittels &-Operator mit -s (Komplement) verbunden. Danach wird s durch das Ergebnis der voherigen Rechnung geteilt. Wie bereits gesagt, entsprichteine Straße einer Folge von fünf Einsen, was dezimal 31 entspricht. Somit können wir obige Berechnung direkt abfragen.


(s/(s&-s) == 31)

Wird dieser Ausdruck wahr, liegt bei der Hand eine Straße vor. Dennoch fehlt noch ein Fall zur Bildung einer Straße, der von obiger Berechnung nicht abgedeckt wird. Und zwar jener, wenn die Straße mit einem Ass beginnt (Ass -> 2 -> 3 -> 4 -> 5). In unserer Binärabbildung entspräche dies 1 0000 0000 1111. Nach dem Shift (der nicht stattfindet, da es keine rechtsseitigen Nullen gibt) würden wir im Abgleich gegen 31 ein False erhalten.

Da dies jedoch der einzige Sonderfall bei den Straßen ist, können wir ihn direkt abfragen. Der Wert für diese Hand 1 0000 0000 1111 entspricht der Dezimalzahl 16444 welche sich Hexadezimal als 403c darstellen lässt. Somit lässt sich diese Hand mit folgendem Code abfragen:


(s == 0x403c)

Kombiniert man den Sonderfall mit allen anderen Fällen, erhält man zur Abfrage, ob eine Blatt eine Straße ist, folgenden Code:


(s/(s&-s) == 31) || (s == 0x403c)

Wird obiger Gesamtausdruck wahr (=true), dann handelt es sich um eine Straße. Dies bilden wir nun noch auf das Array der möglichen Hände ab. Wie bereits gesagt, hatten wir im Arrays ein paar Indizes freigelassen, für die Straßen sowie den Royal Flush. Nun belegen wir den Index 2 mit der Straße…


hands=["4 of a Kind", "", "Straight", "", "High Card",
"1 Pair", "2 Pair", "", "3 of a Kind", "Full House" ];

…und kombinieren den Code zur Erkennung der für die meisten Hände gilt mit dem neuen Code für die Straße.

v = v % 15 - ((cardValuesField/(cardValuesField&-cardValuesField) == 31) || (cardValuesField == 0x403c) ? 3 : 1);

Wir erinnern uns: v%15 (- 1) ergab den Index für die Erkennung fast aller Hände. Nun ziehen wir davon folgenden Ausdruck ab:


((cardValuesField/(cardValuesField&-cardValuesField) == 31) || (cardValuesField == 0x403c) ? 3 : 1)

Hierbei nutzen wir den tenären Operator. Dieser gibt den Wert hinter dem ? zurück, wenn der Ausdruck vor dem Fragezeichen wahr ist und den Wert hinter dem Doppelpunkt, wenn der Ausdruck falsch ist.

Ist der Ausdruck falsch, liegt keine Straße vor, geben wir eine 1 zurück. Von v%15 wird also 1 abgezogen. Das ergibt die Rechnung, die wir für fast alle Hände brauchen.

Ist der Ausdruck wahr, weil eine Straße vorliegt, geben wir eine 3 zurück. Liegt eine Straße vor, dann haben wir in v auch fünf Werte mit dem Zähler jeweils auf 1 (0001 + 0001 + 0001 + 0001 + 0001) was 5 ergibt. Zieht man hiervon die 3 ab, erhält man 2 – den Index, in dem “Straight” im hands-Array steht.

Kommen wir zum letzten Schritt. Die Erkennung von Flushes…

Flush, Straigt Flush und Royal Flush

Bisher haben wir nur mit einem der beiden Eingabewerte der Hauptfunktion gearbeitet. Und zwar mit dem cardRanks-Array (in der Originalfunktion “cs”) welches die Kartenwerte der 5 Pokerkarten enthält. Nun benötigen wir das zweite Eingabearray namens cardSuits (in der Originalfunktion “ss”).

Dieses Array enthält die Kartenfarben der 5 Kartern in der Hand. Denn ein Flush besteht per Definition aus fünf Karten der gleichen Spielfarbe.

Für die Ermittlung eines Flushs nutzen wir wieder einmal den OR-Operator. Wenn man zwei identische Zahlen ORed, dann erhält man als Ergebnis die Ausgangszahl. Wendet man den OR-Operator auf zwei unterschiedliche Zahlen an, so erhält man eine komplett andere Zahl.

Für einen Flush müssen alle Farben (die in dem cardSuits-Array als Zahlen übergeben werden) gleich sein. Deshalb kann man einfach überprüfen, ob die Farbe der ersten Karte dem ge-OR-ten Wert der restlichen vier Karten entspricht.


cardSuit[0] == (cardSuit[1] | cardSuit[2] | cardSuit[3] | cardSuit[4]);

Ist obiger Ausdruck wahr, dann handelt es sich um einen Flush.

Doch auch hier gibt es (wie bei den Straßen) eine Ausnahme. Ein Royal-Flush steht noch über dem Flush. Deshalb müssen wir diesen Explizit behandeln. Hierzu vergleichen wir den 13-bittigen String, wie bei den Straßen, wieder mit einem Festwert. (Der Wert für einen Royalflush entspricht in hexadezimaler Darstellung 7c00.)

Abschließend füllen wir unserer hands-Array noch mit den fehlenden Händen:


hands=["4 of a Kind", "Straight Flush", "Straight", "Flush", "High Card",
"1 Pair", "2 Pair", "Royal Flush", "3 of a Kind", "Full House" ];

Die Zeilen zur Berechnung der hands-Array Indexes sehen nun wie folgt aus:


v = v % 15 - ((cardValuesField/(cardValuesField&-cardValuesField) == 31) || (cardValuesField == 0x403c) ? 3 : 1);
v -= (cardSuit[0] == (cardSuit[1]|cardSuit[2]|cardSuit[3]|cardSuit[4])) * ((cardValuesField == 0x7c00) ? -5 : 1);

Nachdem wir in der ersten Zeile den Index v für alle normalen Hände und Straßen berechnet haben, passen wir diesen in der zweiten Zeile für Flushs noch einmal an.

Die Anpassung findet jedoch nur statt, wenn der Ausdruck vor dem *-Zeichen größer 0 ist. Liegt kein Flush vor, so ergibt (ss[0] == (ss[1]|ss[2]|ss[3]|ss[4])) false, was einer 0 entspricht. Die Anpassung wird also nur vorgenommen, wenn ein Flush vorliegt.

Die Höhe der Anpassung wird durch den Ausdruck hinter dem *-Zeichen definiert. Handelt es sich um einen Royal-Flush (s == 0x7c00), dann wird v um 5 erhöht (–5), ist es kein Royal-Flush, wird v um 1 verringert.

Die Verschiebung um +5 bzw. -1 funktioniert aus folgenden Gründen. Ein Royal-Flush ist zeitgleich auch eine Straße. Liegt ein Royal-Flush vor, muss also bereits eine Straße ermittelt worden sein. v steht somit auf 2, dem Index für “Straight” im hands-Array. 2+5 = 7 und entspricht dem Index für “Royal Flush”.

Wurde nur Flush ermittelt, also ohne “Royal” so kann dies nur aus zwei Händen hervorgehen. Einer Straße oder einer High-Card. Alle anderen Hände können per Definition keinen Flush ergeben.

Liegt eine Straße vor steht v auf 2. Zieht man 1 ab, erhält man 1, den Index für “Straight Flush”. Liegt eine High-Card vor, ist v = 4. Zieht man 1 ab, landet bei 3, dem Index für “Flush”.

Und das war es auch schon. Nun können wir sämtliche Hände erkennen. Mehr ist nicht nötig.

Abschluss & Fazit

Ich gebe zu, der Artikel hat mich wesentlich mehr Zeit gekostet als vermutet. Ich hoffe jedoch, dass er den Zeiteinsatz wert war und ihr, als Leser, den Erklärungen folgen konntet und nun auch versteht wie dieses kleine Meisterstück an Javascript-Code funktioniert.

Abschließend zeige ich euch noch einmal den gesamten Code dieses Artikels. Doch zuvor noch ein, zwei Fragen. Kennt ihr ähnliche, kompakte Code-Snippets? Seid ihr selber schon einmal über so einen “Schatz” gestolptert? Wenn ja, dann ab in die Kommentare damit!

Sollten noch Fragen offen sein oder etwas unklar, freue ich mich ebenfalls über eure Kommentare.

Und nun – wie versprochen – der gesamte Code:


function rankPokerHand(cardRank,cardSuit) {
  var cardCountBitField = 0, i, currCount, v;

  //BitArray für die vorhandenen Kartenwerte (vgl. Format 2)
  var cardValuesField = 1<<cardRank[0] |1<<cardRank[1]|1<<cardRank[2]|1<<cardRank[3]|1<<cardRank[4];
 
  //BitArray für die Anzahl der einzelnen Kartenwerte (vgl. Format 1)
  for (i=0; i<5; i++) {
    currCount = getCurrentCountForCardRank(cardCountBitField, cardRank[i]);
    cardCountBitField = addValueToCardRank(cardCountBitField, cardRank[i], currCount + 1);
  }
  
  //normale Hände und Straßen ermitteln
  v = v % 15 - ((cardValuesField/(cardValuesField&-cardValuesField) == 31) || (cardValuesField == 0x403c) ? 3 : 1);

  //Flush, Royal-Flush und Straigt-Flush ermitteln
  v -= (cardSuit[0] == (cardSuit[1]|cardSuit[2]|cardSuit[3]|cardSuit[4])) * ((cardValuesField == 0x7c00) ? -5 : 1);

  document.write("Hand: " + hands[v] + (suitBitField == 0x403c ? " (Ace low)" : "")+"
");
}


function getPositionForCardRank(cardRank) {  
  return Math.pow(2,cardRank*4);
}

function getCurrentCountForCardRank(array, cardRank) {
  var position = getPositionForCardRank(cardRank);
  return array/position&15;
}

function addValueToCardRank(array, cardRank, value) {
  var position = getPositionForCardRank(cardRank);
  return array + position * value;
}
3

Best Practice: 1 und 0 nach true und false konvertieren in Javascript

1 und 0 zu true und false in JavascriptHeute mal nur einen ganz kurzen Beitrag. Es gibt sicherlich viele Wege 0 und 1 in Javascript nach true und false umzuwandeln.

Folgender Weg, den ich heute gesehen hab, dürfte aber wohl der kürzeste und auch eleganteste sein. Zudem klappt er nicht nur für Integer-Werte, sondern auch für die String-Repräsentationen von 0 und 1.

Sowas verbuche ich hier immer unter “Programmierperlen”…

0 und 1 nach true und false

Um die Integer 0 und 1 nach Boolean zu konvertieren, genügt es den Not-Operator doppelt einzusetzen.

var thisIsFalse = !!0; //false
var thisIsTrue = !!1; //true

Warum das funktioniert? Nehmen wir folgendes Beispiel. 1 ist ein valider Wert und somit true. Wenn wir nun !1 schreiben, dann negieren wir das true und erhalten false. Mit einem zweiten ! also !!1 negieren wir das erhaltene false und bekommen somit true für 1. Für die 0 sieht es genau andersrum aus.  So entspricht 0 einem false. Dementsprechen !0 einem true und !!0 einem false.

“0”- und “1”-Strings  nach true und false

Und was, wenn 0 und 1 als Strings vorliegen, weil Sie zum Beispiel einem schlecht umgesetztem JSON-String entspringen? Nichts einfacher als das. Mittels des + Operators lassen sich Strings zu int casten. Dies Lösung für einen String sieht also wie folgt aus:

</span>
var thisIsFalse = !!+"0"; //false
var thisIsTrue = !!+"1"; //true

Drei Zeichen, um aus einem String (mit dem Wert “0” oder “1”) einen Boolean zu machen. Ich denke viel eleganter geht es nicht mehr. Aber man lernt ja nie aus. Kennt ihr eine noch bessere Lösung?

0

How to: Poker Hand Evaluator in C# implementieren

Pokerhand Evaluator CsharpDas Thema Poker habe ich nun schon in dem ein oder anderen C#-Artikel als Aufhänger genommen, um tiefer in verschiedene Programmierthematiken einzusteigen. So gab es bereits einen Artikel zur Bildanalyse, einen zur Berechnung der Gewinnwahrscheinlichkeit und einen, in dem die beiden eben genannten kombiniert wurden.

Alle drei Artikel haben eines gemeinsam. Sie basieren auf einer Bibliothek, die ermitteln kann, welche Pokerhand in einem direkt Vergleich der Gewinner ist. Solch eine Bibliothek nennt man auch “Hand evaluator”, da sie zwei Pokerhände evaluieren kann. Und genau hier wollen wir heute ansetzen.

In diesem Artikel wollen wir Schritt für Schritt eine eigene C#-Bibliothek zum Vergleich zweier Pokerhände erstellen. Hierzu arbeiten wir uns nach und nach zum Ziel. Beginnen werden wir mit den Datenstrukturen für Spielkarten und Pokerhände. Abschließend geht es an den Abgleich – also die Ermittlung des Gewinners. Die nachfolgende Implementierung ist angelehnt an die Umsetzung von C. Scutaru.

Die Basis – Datenstrukturen für Spielkarte und Pokerhand

Warum nicht “Datenklassen”? Weil wir eben nicht nur Klassen, sondern auch klassische Strukturen nutzen. Für die Abbildung der Spielkarten nehmen wir ein struct, da die Spielkarte nur zwei enums enthält und diese nach der Initialisierung sowieso keine Änderung mehr erfahren.

public enum TypeOfRank : int 
{ 
	Two = 2, Three, Four, Five, Six, Seven, Eight, Nine, Ten, Jack, Queen, King, Ace 
}

public enum TypeOfSuit : int 
{ 
	Spades, Hearts, Diamonds, Clubs 
}
 
public struct Card
{
	public Card(TypeOfRank rank, TypeOfSuit suit) : this()
 	{ 
 		Rank = rank; 
 		Suit = suit; 
 	}
 
 	public TypeOfRank Rank { get; private set; }
 	public TypeOfSuit Suit { get; private set; }
}

Zuerst definieren wir also die möglichen Kartenwerte (Ranks) und Kartenfarben (Suits) in zwei Aufzählungen (enum), und nutzen diese dann, um unsere Kartenstruktur (struct Card) aufzubauen. Eine Card kann nur beim initialisieren über den Konstruktur mit Farbe und Wert versehen werden. Danach können Farbe und Wert nur noch ausgelesen werden, da der setter mittels “private set” nach außen hin nicht sichtbar ist.

Kommen wir nun zur Pokerhand. Diese implementiert das IComparable-Interface, sodass die Klasse zwingend das eine CompareTo-Methode enthalten muss. Zudem implementieren wir noch eine Sort-Methode.

public class PokerHand : IComparable<PokerHand>
{
 	public Card[] Cards { get; private set; }
 
	public PokerHand(Card c1, Card c2, Card c3, Card c4, Card c5)
 	{ 
 		Cards = new Card[] {c1, c2, c3, c4, c5}; 
 		Sort(); 
 	}
 
	private void Sort()
	{
 		Cards = Cards.OrderBy(c => c.Rank)
  					 .OrderBy(c => Cards.Where(c1 => c1.Rank == c.Rank)
  					 .Count()).ToArray();
 
 		if (Cards[4].Rank == TypeOfRank.Ace 
 		    && Cards[0].Rank == TypeOfRank.Two
  			&& (int)Cards[3].Rank - (int)Cards[0].Rank == 3)
  		{
  			Cards = new Card[] { Cards[4], Cards[0], Cards[1], Cards[2], Cards[3] };
  		}
  		
	}
 
	public int CompareTo(PokerHand other)
	{
 	for (var i = 4; i >= 0; i--)
 	{
  		TypeOfRank rank1 = Cards[i].Rank, 
  				   rank2 = other.Cards[i].Rank;
  		if (rank1 > rank2)
  			return 1;
  		if (rank1 < rank2)
  			return -1;
 	}
 	return 0;
}

Das Herzstück der PokerHand-Klasse ist das Cards-Array. Dieses enthält die Spielerkarten (vom eben erstellen Card-Struct-Typ). Befüllt wird das Cards-Array im Konstruktur woraufhin es mit der Sort-Methode sortiert wird.

Die Sort-Methode ist die erste von zwei Funktionen der PokerHand-Klasse. Sie sortiert die Karten ihrem Wert nach. Durch den zweiten Teil des Lambda-Ausdrucks (“.OrderBy(c = Cards.Where…”) werden Pärchen und Drillinge höher einsortiert. Die abschließende if-Abfrage prüft, ob eine Straße vorliegen kann und schiebt ggf. das Ass an den Anfang der Hand.

Die CompareTo-Methode erlaubt nun ein einfaches Vergleichen zweier Pokerhände, da diese bereits durch die Sort-Methode ihrem Wert nach korrekt sortiert sind. Die genaue Wertreihenfolge kann zum Beispiel auf pokervergleich.net nachgeschlagen werden, wo über Poker, Pokeranbieter und Poker Boni informiert wird.

Implementierung der Pokerhand-Typen

Was fehlt bisher? Na klar, die einzelnen Pokerhand-Typen wie Flush, Full house oder Straight. Da es sich hierbei wieder um gegebene Werte/Kombinationen handelt, greifen wir wieder auf den enum-Typ zurück. Dabei starten wir mit der stärksten und enden mit der schwächsten Hand.

public enum HandType : int 
{ 
	RoyalFlush, StraightFlush, FourOfAKind, FullHouse, Flush, Straight, ThreeOfAKind, TwoPairs, OnePair, HighCard 
}

Da es sich bei dem enum, jedoch nur um eine numerische Auflistung handelt, müssen wir noch definieren, was den jeweiligen Typen eigentlich ausmacht. Hierzu schreiben wir einen Validator – namentlich die IsValid-Funktion.

public bool IsValid(HandType handType)
{	
	switch (handType) 
	{
  		case HandType.RoyalFlush: 
   			return IsValid(HandType.StraightFlush) && Cards[4].Rank == TypeOfRank.Ace;
  		case HandType.StraightFlush:
   			return IsValid(HandType.Flush) && IsValid(HandType.Straight);
  		case HandType.FourOfAKind:
   			return GetGroupByRankCount(4) == 1;
  		case HandType.FullHouse:
   			return IsValid(HandType.ThreeOfAKind) && IsValid(HandType.OnePair);
  		case HandType.Flush:
   			return GetGroupBySuitCount(5) == 1;
  		case HandType.Straight:
   			return (int)Cards[4].Rank - (int)Cards[0].Rank == 4
    				|| Cards[0].Rank == TypeOfRank.Ace;
  		case HandType.ThreeOfAKind:
   			return GetGroupByRankCount(3) == 1;
  		case HandType.TwoPairs:
   			return GetGroupByRankCount(2) == 2;
  		case HandType.OnePair:
   			return GetGroupByRankCount(2) == 1;
  		case HandType.HighCard:
   			return GetGroupByRankCount(1) == 5;
 	}
 	return false;
}
 
private int GetGroupByRankCount(int n)
{ 
	return Cards.GroupBy(c => c.Rank).Count(g => g.Count() == n); 
}
 
private int GetGroupBySuitCount(int n)
{ 
	return Cards.GroupBy(c => c.Suit).Count(g => g.Count() == n); 
}

Mittels einer Switch-Case-Abfrage werden die einzelnen Handtypen durchgegangen und gemäß der Pokerregeln gegen die in der Hand befindlichen Karten (im Cards-Array) geprüft. Stimmt der übergebene Hand-Typ mit den Karten im Cards-Array überein, gibt die IsValid-Methode ein “true” zurück.

Der eigentliche Hand-Evaluator

Der letzte Teil der Implementierung besteht aus der Ausprogrammierung der Evaluate-Methode vom Interface-Typ IList. Die Evaluate-Methode nimmt ein Dictionary vom Typ <string, Pokerhand> an, wobei als String der Name des jeweiligen Handbesitzers angegeben werden sollte. Der Rückgabewert der Funktion entspricht einer Namensliste der Gewinner.

public static IList<string> Evaluate(IDictionary<string, PokerHand> hands)
{
	var len = Enum.GetValues(typeof(HandType)).Length;
	var winners = new List<string>();
	HandType winningType = HandType.HighCard;
 
 	foreach (var name in hands.Keys)
 	{
 		for (var handType = HandType.RoyalFlush;
   			(int)handType < len; handType = handType + 1) { var hand = hands[name]; if (hand.IsValid(handType)) { int compareHands = 0, compareCards = 0; if (winners.Count == 0 || (compareHands = winningType.CompareTo(handType)) > 0
	     			|| compareHands == 0
		      		&& (compareCards = hand.CompareTo(hands[winners[0]])) >= 0)
		    	{
		      		if (compareHands > 0 || compareCards > 0)
		      			winners.Clear();
		      		winners.Add(name);
		      		winningType = handType;
	    		}
		    	break;
		   	}
  		}
 	}  
 	return winners;
}

Innerhalb der Evaluate-Methode wird für jeden Spieler die gegebene Hand gegen alle möglichen Hände mittels der IsValid-Methode geprüft. Die ermittelte Hand wird wiederum gegen die Hand des/der Spieler in der Gewinnerliste (winner) geprüft. Ist die aktuelle Hand besser wird, die Gewinnerliste geleert und der aktuelle Spieler eingesetzt. So bleiben am Ende ein (oder bei gleichen Händen / Split-Pot) mehrere Gewinner, die von der Evaluate-Methode zurückgegeben werden.

5

PDFs im Browser erstellen, konvertieren und signieren

webPDF LogoKostenlose PDF-Reader gibt es wie Sand am Meer. Egal ob Acrobat Reader, Foxit, NitroPDF oder Sumatra. Die Auswahl groß und oftmals auch für den gewerbsmäßigen Gebrauch kostenlos freigegeben. Sobald es jedoch daran geht, aktiv mit PDF-Dateien zu arbeiten – also eben solche zu erstellen, umzuwandeln, zu verändern oder zu signieren – ist Schluss mit der bunten Vielfalt und aus kostenlos wird schnell sehr, sehr teuer.

Ok, wer die Nerven besitzt und vielleicht nur ein zwei, drei “Mann”-Büro hat, der mag mit einer Mischung aus mehreren Freeware-Tools und ein bisschen Opensource seinen eigenen Workflow basteln können, doch sobald es ins professionelle Umfeld geht, kann dies auch nicht mehr die präferierte Lösung sein.

Ist die “Make or Buy”-Entscheidung gefallen und es soll gekauft werden, bleibt die Frage: “Welches Produkt erfüllt die Anforderungen?” Im heutigen Artikel möchte ich deshalb einmal die Lösung von webpdf.de vorstellen, welche (entgegen der eingangs aufgezählten Produkte) einen etwas anderen Ansatz verfolgt.

webPDF – Firmendokumente im Netz?

Ok, die Zwischenüberschrift ist provokant gewählt, denn kaum eine Sicherheitsrichtlinie lässt es zu, die eigenen Dokumente in fremde Hände im Netz zu leiten. Doch Web ist nicht gleich Internet. Denn das “web” in webPDF besagt lediglich, dass es sich um eine webbasierte Lösung handelt. Entgegen der Überschrift lässt sich webPDF nämlich auch im lokalen (Intra)-Net hosten, sodass die sensiblen Daten keineswegs die Firmeninfrastruktur verlassen müssen.

Und genau an diesem Punkt sind wir auch schon an dem wesentlichen Unterscheidungsmerkmal zwischen webPDF und den Konkurrenzangeboten von Nitro, Foxit oder Adobe. Dadurch, dass webPDF auf einem Server läuft und die Nutzer per Browser damit arbeiten ist nur eine Installation nötig. Diese Philosophie pflegt webPDF sogar bis in die Lizenzierung. Gut erklärt wird dies auch in folgendem Video.

Bezahlt wird nur einmal für den Server. Keine fortlaufenden Kosten und keine Gebühr pro Nutzer. Doch bevor wir uns genauer mit den Kosten befassen, werfen wir noch einen Blick auf die Funktionen des Tools.

Was bietet webPDF und was bietet es nicht?

Neben vielen anderen Punkten, bietet webPDF folgende Funktionen:

  • Konvertierung von über 100 Dateiformaten nach PDF
  • HTML und Mails (Domino und Exchange) können nach PDF konvertiert werden
  • Signierung, Zertifizierung und Langzeitarchivierung mit Timestamps
  • PDF/A (nach ISO 19005) erstellen und prüfen
  • Texte per OCR lesbar machen
  • Split, merge und cut von PDF-Dokumenten
  • Schnittstellen zur Integration in bestehende Softwaresysteme

Das einzige Feature, welches webPDF nicht bietet, ist die inhaltliche Bearbeitung von bestehenden PDF-Dokumenten. Das neu setzen bestehender Dokumente oder die Überarbeitung von PDF-Dokumenten ist somit nicht möglich.

Wer sich mit dem Funktionsumfang näher vertraut machen möchte, kann dies z.B. über die angebotene Online-Demo.

Kosten – wann lohnt sich der Einsatz?

Kommen wir nun wohl zu einer der spannendsten Fragen im geschäftlichen Umfeld: “Wie viel kostet so eine Serverlizenz?” Diese Frage wird ebenfalls direkt auf der Homepage beantwortet. Der Server für den firmeninternen Gebrauch liegt bei 3.990 €. Was auf den ersten Blick viel erscheint, relativiert sich jedoch je nach Einsatzszenario recht schnell. Hierzu habe ich einmal folgendes Diagramm erstellt, welches die Kostenentwicklung im ersten Jahr für die verschiedenen Produkte bei unterschiedlichen Nutzerzahlen anzeigt.

webPDF Kostenvergleich

Wie im Diagramm gut zusehen ist, ist ab spätestens 30 Nutzern webPDF, zumindest vom Preis her, unschlagbar. Eben genannte Einschränkung bezieht sich natürlich darauf, dass in der Praxis im Einzellfall für das eigene Unternehmen evaluiert werden muss, ob webPDF auch wirklich allen Anforderungen genügt.

Denn was bringt die Kostenersparnis, wenn am Ende ein Kernfeature des Lastenhefts fehlt? Nichts desto trotz ist die preisliche Entwicklung denke ich gut zu erkennen und ein starkes Argument für webPDF.

Fazit

Es hat Spaß gemacht einmal über den sprichwörtlichen Tellerrand zu schauen. Neben den “Big Players” wie Adobe gibt es noch viele andere Lösungen, die es zu entdecken gilt. Denn nicht immer muss die bekannteste Marke auch das beste Produkt vertreten. Hier hilft nur eines: Suchen, analysieren, evaluieren und die, für den eigenen Business-Case, beste Lösung erarbeiten.

Seite 1 von 65