control-class.php

Praktisch jedes Skript, das durch ein print2forms-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 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.

Quellcode

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:

control-class.php
<?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 */
      }
  }
?>


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 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");


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. 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");


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. 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)


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. 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"]}");


Hinweise



1)
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.

2)
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!

3)
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.

4)
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.

5)
Siehe das Beispiel für die Methode setIndex weiter oben.

6)
Dabei werden die Zeilenfragmente in der Reihenfolge innerhalb der Kontrolldatei aneinandergefügt, was unter bestimmten Umständen nicht unbedingt der Reihenfolge im Druckbild entspricht!

7)
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.

8)
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.

9)
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.

10)
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.