===== control-class.php ===== Praktisch jedes Skript, das durch ein (p2f)-Gateway aufgerufen wird, analysiert den Druckauftrag, um Entschei(-)dungen 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 ((Die Kontrolldatei ist eine reine Textdatei, die immer in der **Kodierung Unicode UTF-8** vorliegt. Damit ist sichergestellt, dass auch nicht lateinische Alphabete sicher erkannt und ausgewertet werden können. \\ \\ )), 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. ((Die Auflösung der Positionen ist so gewählt, dass auch grosse Papierformate sicher bearbeitet werden können. Von daher kann es bei 600 Punkten pro Zoll und mehr schon vorkommen, dass Zeilen, die nur geringfügig voneinander abweichen mit gleichem Index in der Kontrolldatei auftauchen!\\ \\ )) 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**. \\ \\ ===== Quellcode ===== Die im Quellcode nachfolgend dargestellte Klasse übernimmt eine Reihe von immer wiederkehrenden Aufgaben im Umgang mit Kontrolldateien. Als ersten Überblick sind dies: * Einlesen der Kontrolldatei in eine interne Struktur für den schnellen, und wahlfreien Zugriff auf die Inhalte der Kontrolldatei. Methoden für den wahlfreien Zugriff auf einzelne Zeilen, sowie Methoden für den sequenziellen Zugriff auf Zeilen. * Methoden zur Extraktion von Einzelinformationen via regulärer Ausdrücke aus der Kontrolldatei, die dann abgespeichert werden können. * Eine Methode, um in einer Vorlage die Platzhalter (durch '@' eingeklammert) gegen aufgesammelte Daten aus der Kontrolldatei auszutauschen. Das erleichtert z.B. die Erstellung personaliserter Anschreiben. * Eine Methode, um aus der vom Gateway ebenfalls erzeugten %%PCL%%-Datei eine %%PDF%%-Datei zu erstellen. Dabei können auch die Meta-Daten für das %%PDF%%-Dokument (Autor, Titel, Schlüsselworte, etc) gesetzt werden. * Anlegen und Fortschreiben einer Log-Datei mit Fehlermeldungen, um für den Supportfall nachvollziehen zu können, ob bei der Abarbeitung von Skripten Probleme aufgetreten sind. Das Skript sollte vorzugsweise über diesen Link {{print2forms:tips:control-class.php|control-class.php}} geladen werden, weil beim Laden über den Reiter des Quellcodes die Unicode-BOM verlorengeht. ((Für die korrekte Funktion der Skripte ist es unumgänglich, dass Unicode zum Einsatz kommt. Die Kontrolldatei ist eine in UTF-8 kodierte Datei und somit müssen auch die zur Verarbeitung genutzten Skripte Unicode unterstützen. Ansonsten gibt es erhebliche Probleme mit länderspezifischen Zeichen.\\ \\ )) Bevor nun die einzelnen Methoden beschrieben werden, hier die vollständige Klasse: \\ \\ 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 */ } } ?> \\ ==== Konstruktor ==== 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"); \\ ==== Destruktor ==== 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; \\ ==== error ==== 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'"); \\ ==== errorsFound ==== 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); \\ ==== setIndex ==== 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"); /* ... */ } \\ ==== getIndex ==== 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. \\ \\ ==== fetch ==== 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 ((Der reguläre Ausdruck sollte nur eine Klammer zur Extraktion von Texten enthalten. Zusätzliche Klammern werden ignoriert. Die Angabe von Zeilenanfang **'^'** oder Zeilenende **'$'** ist zulässig. Der Ausdruck wird immer als Unicode ausgewertet.\\ \\ )). 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. ((Siehe das Beispiel für die Methode //setIndex// weiter oben.\\ \\ )) $p2f->fetch ("0101720846", $vars, " +(\d\d\.\d\d\.\d{4})", "InvoiceDate"); \\ ==== getLine ==== 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. ((Dabei werden die Zeilenfragmente in der Reihenfolge innerhalb der Kontrolldatei aneinandergefügt, was unter bestimmten Umständen nicht unbedingt der Reihenfolge im Druckbild entspricht!\\ \\ )) ... [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"); \\ ==== substitute ==== 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. ((Werden Platzhalter benutzt, zu denen es keine passenden Schlüssel im Array gibt, werden diese nicht ersetzt, und der Platzhalter inklusive der '@'-Zeichen bleibt Bestandteil der zurückgebenen Zeichenkette.\\ \\ )) $p2f->substitute ("Sehr geehrte Damen und Herren,

" . "vielen Dank für Ihren Auftrag @OrderNo@.

" . "Mit dieser Mail erhalten Sie dazu unsere Rechnung @InvoiceNo@
" . "mit Datum vom @InvoiceDate@ im Format PDF/A.

" . "Mit freundlichen Grüßen
" . "Ihre Muster GmbH


", $vars)
\\ ==== createPdf ==== 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. ((Darüber, wie Schlüsselwörter voneinander zu trennen sind, macht die %%PDF%%-Spzifikation keine genauen Vorgaben. Es hängt wohl letztlich auch davon ab, wie später diese Informationen aus der %%PDF%%-Datei extrahiert und weiterverarbeitet werden sollen. Semikolon scheint zumindest keine schlechte Wahl.\\ \\ )) Die Konvertierung von %%PCL%% nach %%PDF%% erfordert die Installation zweier Hilfsprogramme. Aus lizenzrechtlichen Gründen dürfen die Programme [[https://www.ghostscript.com/releases/gpcldnld.html|GhostPCL]] und [[https://www.ghostscript.com/releases/gsdnld.html|GhostScript]] der Firma Artifex nicht mit (p2f) zusammen ausgeliefert werden. ((Nachdem die Programme von den hier angegebenen Web-Adressen geladen wurden, müssen für GhostPCL die beiden Dateien //gpcl6win64.exe// und //gpcl6win64.dll// (für 64-Bit Systeme, ansonsten //gpcl6win32.exe// und //gpcl6win32.dll// für 32-Bit Systeme) in den Unterordner //converter// kopiert werden. Für GhostScript müssen die beiden Dateien //gswin64.exe// und //gsdll64.dll// (für 64-Bit Systeme, ansonsten //gswin32.exe// und //gsdll32.dll// für 32-Bit Systeme) in den Unterordner //converter// kopiert werden.\\ \\ )) ((Die Nutzung von **GhostScript** ist für das eigentliche Erzeugen der %%PDF%%-Datei nur insofern von Nutzen, als es eine fehlerfreie Einbettung der %%PDF%%-Metadaten via **PostScript** erlaubt. Neuere Version von GhostPCL (V10.x.x) haben mit der Einbettung via %%PJL%% leider Probleme. Falls diese einmal behoben sein sollten, könnte dann auch auf GhostScript verzichtet werden.\\ \\ )) $pdf = $p2f->createPdf ("$spool\\$document.pcl", "Lieferant GmbH", "Rechnung {$vars ["InvoiceNo"]}", "{$vars ["InvoiceNo"]}; {$vars ["InvoiceDate"]}; {$vars ["CustomerNo"]}"); \\ === Hinweise === * Die Klasse ist jetzt nicht so programmiert, dass sie alle möglichen Fehlnutzungen ausschliesst. Zum Beispiel werden Positionsindices nicht auf Korrektheit geprüft. Und auch manch anderer falsche Parameter mag schwerwiegende Fehler produzieren. Das ist von uns bewusst in Kauf genommen worden, um einerseits die Komplexität und auch die Laufzeit im Rahmen zu halten. * Sollte das vom Gateway aufgerufene Skript Daten auch aus externen Quellen erfassen, ist aus Gründen der **Betriebssicherheit** eine sehr viel **genauere Kontrolle** der gelesenen Daten angeraten. \\ \\