Praktisch jedes Skript, das durch ein print2forms-Gateway aufgerufen wird, analysiert den Druckauftrag, um Entscheidungen treffen zu können und um Daten für die weitere Verarbeitung des Druckauftrags zu ermitteln. Dazu wird vom Gateway immer auch eine Kontrolldatei mit der Endung '.ctl' erzeugt 1), in der die Metadaten des Druckauftrags und die erkannten Textbestandteile im Auftrag enthalten sind.
Am Anfang jeder Zeile befindet sich in eckigen Klammern eingeschlossen eine Kennung oder ein Positionsindex. Die Kennungen kennzeichnen die Meta-Daten des Druckauftrags, z.B. wer den Auftrag wann und mit welchem Formular gedruckt hat. Ein Positionsindex ist eine zehnstellige Hexadezimalzahl, deren erste zwei Stellen eine Seitennummer darstellen. Die nächsten vier Stellen entsprechen der vertikalen Position des nachfolgenden Textes, die lezten vier Stellen entsprechen der horizontalen Position des nachfolgenden Textes. 2)
Wie im nachfolgenden Beispiel zu sehen, kann eine vertikale Position mehrfach auftauchen, wenn die Texte innerhalb einer Zeile durch absolute oder relative Positionierungen angeordnet werden. Auch entspricht in manchen Fällen die Reihenfolge der Texte in der Kontrolldatei nicht immer der optischen Reihenfolge auf dem Papier.
[p2f]386270721 [Gateway]3.5.1.3091;IS2YS00.XML [Param]3.0;192.168.13.254 [Time]16.02.24 02:23:34 [User]PP_BATCH_2C [Job]SCRIPT LK01 FERT-LK01-27 [Hold]NO [Save]NO [Form]SCRIPT LK01 FERT-LK01-27 [Process]IS2_NR9.XML [01012D0605]Rechnungsnummer [01012D0846]471102 [0101720605]Rechnungsdatum [0101720846]05.06.2018 [01036C00C6]Hans Müller [0103B100C6]Kundenstrasse 46 [0103F600C6]35268 Musterdorf [01043B00C6]Deutschland ...
Von daher ist die Analyse der Kontrolldatei hin und wieder eine anspruchsvolle Aufgabe, die Unterstützung durch vorgefertigte Progammteile erfordert. Dazu dient die hier vorgestellte PHP-Klasse mit dem Namen control-class.
Die im Quellcode nachfolgend dargestellte Klasse übernimmt eine Reihe von immer wiederkehrenden Aufgaben im Umgang mit Kontrolldateien. Als ersten Überblick sind dies:
Das Skript sollte vorzugsweise über diesen Link control-class.php geladen werden, weil beim Laden über den Reiter des Quellcodes die Unicode-BOM verlorengeht. 3)
Bevor nun die einzelnen Methoden beschrieben werden, hier die vollständige Klasse:
<?php class control { private $errorCount = 0; /* number of error messages sent */ private $index = 0; /* line index from control file */ private $matches = array (); /* info from control file */ /*----------------------------------------------------------------------------*/ public function __construct ($zone, $ctlPath) { date_default_timezone_set ($zone); /* prepare log file time stamping */ if (! file_exists ($ctlPath)) $this->error ("Control file missing '$ctlPath'", 1); $ctl = file_get_contents ($ctlPath); /* ctl file coded in UTF-8 */ preg_match_all ("/\[([0-9a-zA-Z]{3,10})\](.+)\r/u", $ctl, $this->matches, PREG_SET_ORDER); } public function __destruct () { if (file_exists ("temp.pdf")) unlink ("temp.pdf"); if (file_exists ("result.pdf")) unlink ("result.pdf"); if (file_exists ("embedding.ps")) unlink ("embedding.ps"); } /*----------------------------------------------------------------------------*/ public function error ($msg, $severity = 0) { file_put_contents ("control.log", "[" . date ('Y-m-d H:i:s', time ()) . "] " . $msg . "\n", FILE_APPEND); if ($severity != 0) die (); $this->errorCount++; } /*----------------------------------------------------------------------------*/ public function errorsFound () { return $this->errorCount; } /*----------------------------------------------------------------------------*/ public function getIndex () { return $this->matches [$this->index] [1]; } /*----------------------------------------------------------------------------*/ public function setIndex ($lineIndex) { for ($idx = 0; $idx < count ($this->matches); $idx++) if ($this->matches [$idx] [1] == $lineIndex) break; if ($idx == count ($this->matches)) return ""; $this->index = $idx; return trim ($this->matches [$idx] [2]); } /*----------------------------------------------------------------------------*/ public function getLine ($lineIndex) { $line = ""; do { for ($idx = 0; $idx < count ($this->matches); $idx++) if ($this->matches [$idx] [1] == $lineIndex) break; if ($idx == count ($this->matches)) return ""; $lineIndex = $this->matches [$idx + 1] [1]; $lineidx = substr ($this->matches [$idx] [1], 0, 6); $line .= $this->matches [$idx] [2]; } while (substr ($lineIndex, 0, 6) == $lineidx); return $line; } /*----------------------------------------------------------------------------*/ public function fetch ($lineIndex, & $target, $regEx, $idx) { $line = ($lineIndex == "") ? $this->matches [$this->index++] [2] /* current line */ : $this->setIndex ($lineIndex); /* requested line */ if (preg_match ("/" . $regEx . "/u", $line, $m) == 1) $target [$idx] = $m [1]; else $target [$idx] = ""; } /*----------------------------------------------------------------------------*/ public function substitute ($tmpl, $vars) { /* Replace placeholders in template */ foreach ($vars as $key => $value) $tmpl = str_replace( "@$key@" , $value , $tmpl); return $tmpl; } /*----------------------------------------------------------------------------*/ public function createPdf ($pcl, $company, $reference, $keywords) { if (! file_exists ($pcl)) { $this->error ("PCL file missing '$pcl'", 0); return ""; } /* Convert PCL file to PDF/A format using GhostPCL first */ system ("\"converter/gpcl6win64.exe\" \"-otemp.pdf\" " . "-sPDFCompatibillityPolicy=2 -sDEVICE=pdfwrite -dPDFA=1 " . "-dCompressPages=false -dCompressFonts=false \"$pcl\"", $result); if ($result != 0) { $this->error ("Conversion to pdf failed for '$pcl'", 0); return ""; } /* Write postscript file to setup meta data for pdf file creation */ file_put_contents ("embedding.ps", "%!PS\n" . "[ /Title ($company - $reference)\n" . " /Author ($company)\n" . " /Subject (Rechnung im PDF-Format)\n " . " /Keywords ($keywords)\n" . " /Creator (print2forms)\n" . " /DOCINFO\n" . "pdfmark\n" . "%%EOF"); /* Convert PDF/A once again to integrate meta data using GhostScript. This is neccessary because newer versions of gpcl6win64.exe fail in processing a PJL command with meta data. */ $internal = "result.pdf"; /* dafault name for resulting document */ system ("\"converter/gswin64.exe\" " . "-sDEVICE=pdfwrite " . "-dPDFA=1 " . "-o $internal " . "temp.pdf " . "embedding.ps", $result); if ($result != 0) { $this->error ("Appending meta data to pdf failed for '$pcl'", 0); return ""; } return $internal; /* internal name of the pdf file as result */ } } ?>
Der Konstruktor der Klasse benötigt als Parameter eine in der jeweiligen PHP-Installation bekannte Zeitzone sowie den vollständigen Pfad auf die Kontrolldatei.
Die korrekte Zeitzone wird für die Log-Datei benötigt. Falls Fehler auftreten, ist so sichergestellt, dass die in der Log-Datei angegebenen Zeitstempel tatsächlich in der lokalen Zeit sind.
Es wird überprüft, ob der Zugriff auf die Kontrolldatei gegeben ist, und wenn ja, wird die Kontrolldatei komplett in einer internen Struktur abgelegt. Alle Methoden der Klasse greifen nur auf diese interne Struktur zu, es erfolgt kein weiterer Zugriff auf die eigentliche Datei.
$p2f = new control ("Europe/Berlin", "$spool\\$document.ctl");
Hier werden eventuell angelegte Zwischendatei wieder entfernt. Falls es während der Inbetriebnahme eines Skripts hilfreich ist, die Zwischendateien zu analysieren, müssen die betreffenden Zeilen des Destruktors gegebenenfalls auskommentiert werden. Der Destruktor wird implizit beim Ende des Skriptes aufgerufen. Wahlweise kann das auch explizit erfolgen.
$p2f = null;
Die Methode akzeptiert als Parameter eine Zeichenkette, die in die Log-Datei geschrieben wird. Der optionale zweite Parameter sorgt dafür, dass die Ausführung des Skriptes beendet wird, wenn er einen Wert ungleich Null hat. Damit kann bei besonders schwerwiegenden, irreparablen Fehlern (z.B. Fehlen der Kontrolldatei) sofort abgebrochen werden, ohne das jedesmal ausprogrammieren zu müssen.
if ($em->sendmail ($data, $message) == 0) $p2f->error ("Sending e-mail failed '$message'");
Gibt einen internen Fehlerzähler zurück. der mit jeder Instanziierung eines Objekts der Klasse zurückgesetzt wurde. Wird hier ein Wert ungleich Null zurückgeliefert, ist im bisherigen Verlauf des Skriptes zumindest einmal eine Fehlermeldung in die Log-Datei geschrieben worden.
if ($p2f->errorsFound () != 0) exit (-17);
Die Methode setzt einen internen Positionsindex auf den als Parameter übergebenen Wert. Der Parameter muss eine zehnstellig Hexadezimalzahl sein. Dies ist die Vorbereitung für nachfolgende Aufrufe der Methode fetch ohne Positionsindex.
$p2f->setIndex ("01063100FD"); /* position to start with item table */ while ($p2f->getIndex () != "010D1A00C6") /* loop through list of items */ { /* Process list of invoice items */ $p2f->fetch ("", $vars, "(\d+)", "LineID"); $p2f->fetch ("", $vars, "(.*)", "SellerAssignedID"); /* ... */ }
Die Methode gibt den aktuellen Wert eines internen Positionsindex als zehnstellig Hexadezimalzahl zurück. In der Regel wird damit kontrolliert, ob die Methode fetch bereits das Ende des zu lesenden Bereichs erreicht hat.
Die Methode braucht als Parameter zunächst den zu suchenden Positionsindex als zehnstellige Hexadezimalzahl. Zur Speicherung des extrahierten Textes wird danach die Adresse eines Arrays übergeben. Ein regulärer Ausdruck sucht innerhalb des Textes an dieser Position die gewünschte Information 4). Der letzte Parameter definiert einen Namen, unter dem der extrahierte Text im Array abgespeichert wird.
Wird als Parameter für den Positionsindex eine leere Zeichenkette angegeben, wird der interne Positionsindex als Referenz genommen. Gleichzeitig wird der interne Positionsindex auf die nächste Zeile innerhalb der Kontrolldatei weitergeschoben. Damit kann die Kontrolldatei quasi sequentiell bearbeitet werden. 5)
$p2f->fetch ("0101720846", $vars, " +(\d\d\.\d\d\.\d{4})", "InvoiceDate");
Die Methode liefert als Resultat die Zeichenkette am als Parameter übergebenen Positionsindex. Eine Besonderheit von getLine ist, dass tatsächlich eine Zeile zurückgegeben wird. Das wird dadurch erreicht, dass die Methode alle Positionen mit gleichlautendem vertikalen Wert aufsammelt und zu einer Zeile verbindet. 6)
... [010E7300C6]innerhalb 30 Tag [010E730281]en netto bis 04.07.2018, innerhalb 14 Tagen 2% Skonto bis 19.06.2018 ...
$vars ["PaymentTerms"] = $p2f->getLine ("010E7300C6");
Die Methode sucht in einem vorgegebenen Text nach den Bezeichnern aus einer Liste von extrahierten Texten. Als erster Parameter wird der mit Platzhaltern versehene Text übergeben. Platzhalter sind Namen, die in '@'-Zeichen eingeklammert sind. Sie entsprechen den Schlüsseln innerhalb des Arrays mit den aufgesammelten Texten.
Anschliessend geht die Methode sequentiell durch das Array, welches als zweiter Parameter übergeben wird, und ersetzt die Platzhalter, die genau so heissen wie die Schlüssel im Array, gegen den Text der unter diesem Schlüssel abgelegt worden ist. 7)
$p2f->substitute ("Sehr geehrte Damen und Herren,<br/><br/>" . "vielen Dank für Ihren Auftrag @OrderNo@.<br/><br/>" . "Mit dieser Mail erhalten Sie dazu unsere Rechnung @InvoiceNo@<br/>" . "mit Datum vom @InvoiceDate@ im Format PDF/A.<br/><br/>" . "Mit freundlichen Grüßen<br/>" . "Ihre Muster GmbH<br/><br/><br/>", $vars)
Die Methode erzeugt aus der als erstem Parameter übergebenen PCL-Datei eine PDF-Datei. Als zweiter Parameter wird eine Zeichenkette übergeben, die als Autor in den Metadaten der PDF-Datei eingetragen wird. Der dritte Parameter ist eine Zeichenkette, die als Betreff in den Metadaten der PDF-Datei eingetragen wird. Der vierte Parameter ist eine Zeichenkette, die als Schlüsselwörter in den Metadaten der PDF-Datei eingetragen wird. 8)
Die Konvertierung von PCL nach PDF erfordert die Installation zweier Hilfsprogramme. Aus lizenzrechtlichen Gründen dürfen die Programme GhostPCL und GhostScript der Firma Artifex nicht mit print2forms zusammen ausgeliefert werden. 9) 10)
$pdf = $p2f->createPdf ("$spool\\$document.pcl", "Lieferant GmbH", "Rechnung {$vars ["InvoiceNo"]}", "{$vars ["InvoiceNo"]}; {$vars ["InvoiceDate"]}; {$vars ["CustomerNo"]}");