PDF-Dokumente ausschießen

Der Begriff "ausschießen" beschreibt im Feld der Printmedien das Arrangieren von Seiten eines Dokumentes auf einem Druckbogen. Ziel ist es die Einzelseiten so auf dem Druckbogen zu platzieren, dass, nach dem Drucken und Zuschneiden des Bogens, eine Bindung der Seiten stattfinden kann. Das Ergebnis kann eine Broschüre, Heft oder sogar Buch sein.

In typischen Anwendungen für Satz und Layout kann das Ausschießen oder der Broschürendruck mit wenigen Klicks komfortabel eingestellt werden. Was aber, wenn ein vorhandenes Dokument als Heft gedruckt werden soll? Eine solche Aufgabenstellung ist mir begegnet.

Aufgabenstellung

Ein mehrseitiges PDF-Dokument soll als Broschüre gedruckt werden. Das Dokument ist im Format DIN A4 angelegt. Das geschlossene Endformat soll DIN A6 betragen. Auf einem Druckbogen (Blatt DIN A4 Papier) soll beidseitig gedruckt werden. Um einen effektiven Nutzen zu haben, sollen pro Druckbogenseite vier Seiten des Dokumentes gedruckt werden. Es soll eine druckfertige Datei (PDF) der Druckbögen erstellt werden.

Vorhandene Lösungen

Das Netz bietet für den Suchbegriff "imposition" (engl. für ausschießen) in verschiedenen Kombinationen Ergebnisse an. Mir haben weder die Online-Tools noch die Skripte in Python, Tex oder ähnlichem zugesagt. Entweder war das Ergebnis mit Wasserzeichen versehen oder meine Arbeitsumgebung war nicht mit den Skripten kompatibel.

Mein Lösungsansatz

Skizzen

Um das Problem zu verstehen visualisiere ich mir die Druckbögen beispielhaft. Die Skizzen helfen mir beim Denken. Skizze zum Ausschießen eines 24-seitigen Dokumentes

Werkzeuge

Mein Werkzeug der Wahl ist PHP. Hier gibt es mächtige Bibliotheken für die Arbeit mit PDF-Dokumenten. Ich entscheide mich für mPDF

Der Ablauf

Ich werde in zwei Schritten vorgehen, auch, wenn man das Problem in einem Rutsch lösen könnte. Diesen Knoten im Kopf möchte ich jedoch vermeiden (siehe "Nachtrag: Knoten im Kopf gelöst" weiter unten!).

Schritt 1

Die Seiten der Vorlage werden für ein DIN A Format kleiner als die Vorlage ausgeschossen. Wichtig ist, dass die Gesamtseitenzahl durch vier teilbar ist! Jede Seite erhält ihr Pendant (Schön- und Widerdruck) beim doppelseitigen Druck. Die Anzahl der Druckbögen entspricht der Hälfte der Gesamtseitenzahl der Vorlage. Der Druckbogen wird um 90 Grad gedreht (Querformat 297 x 210 [mm]) - DIN Format sei's gedankt - passen die Proportionen einwandfrei.

Die Nummer des Druckbogens wird genutzt, um die platzieren Seiten zu ermitteln. Dabei ist die Unterscheidung von geraden (Schöndruck) und ungeraden (Widerdruck) wichtig. Jeweils müssen die Seiten unterschiedlich angeordnet werden.

Auf einem Schöndruckbogen gilt: Rechts wird die Seite aus der Vorlage platziert, die der Nummer des Druckbogens entspricht; links die Seite der Vorlage, deren Nummer der Summe aus der Gesamtseitenzahl plus eins minus der Nummer des Druckbogens (Gesamtseitenzahl + 1 - Druckbogennummer) entspricht.

Auf einem Widerdruckbogen ist es genau umgekehrt!

Für ein 24-seitiges Dokument würde das ergeben: (Bxx - Bogennummer, G24 - Gesamtseitenzahl)

Bogen 01: Seite 24 (G24 + 1 - B01) | Seite 01 (B01)
Bogen 02: Seite 02 (B02) | Seite 23 (G24 + 1 - B02)
Bogen 03: Seite 22 (G24 + 1 - B03)| Seite 03 (B03)
...
Bogen 10: Seite 10 (B10) | Seite 15 (G24 + 1 - B10)
Bogen 11: Seite 14 (G24 + 1 - B11) | Seite 11 (B11)
Bogen 12: Seite 12 (B12) | Seite 13 (G24 + 1 - B12)

Am Ende dieses Schrittes liegt mir ein Druckbogen für ein geschlossenes DIN A5 Format vor.

Schritt 2

Der erstellte Druckbogen (DIN A5) muss nun auf einen weiteren Druckbogen platziert werden, um das abschließende Format (DIN A6) zu erreichen. Die Anzahl der Druckbögen ergibt sich erneut aus der Hälfte der Gesamtseitenzahl des zuvor erstellten Dokumentes. Der Druckbogen wird erneut um 90 Grad gedreht (Hochformat 210 x 297 [mm]). Nun werden die Seiten oben oder unten platziert.

Hier gilt auf dem Schöndruckbogen: Oben wird die Seite platziert, die der doppelten Nummer des Druckbogens minus eins entspricht; unten wird die gerade ermittelte Seitenzahl um zwei erhöht. Auf dem Widerdruckbogen gilt: Oben wird die Seite platziert, die der doppelten Nummer des Druckbogens minus zwei entspricht; unten wird diese Seitenzahl ebenfalls um zwei erhöht.

Diese Anordnung ermöglicht die Erstellung eines Druckbogens, auch wenn die Gesamtseitenzahl der Vorlage nicht restlos durch acht teilbar ist. Bei einer 20-seitigen Vorlage würde auf den letzten beiden Bögen so jeweils nur die obere Hälfte bedruckt werden.

Zusätzlich muss auf das Vorhandensein von nachfolgenden Seiten geprüft werden, damit keine Leerseiten erstellt werden.

Vorlagen und Beispiele

Hier ein paar Dokumente, die den Prozess etwas veranschaulichen:

Die Druckbögen habe ich mit folgendem Code erstellt.

Mein Code

Das Ergebnis meiner Überlegungen und Tests ist folgender Spaghetti-Code.

Das Dokument aus dem ersten Schritt wird per StreamReader als Vorlage für den zweiten Schritt genutzt.

Die Import-Funktion von mPDF kann keine komprimierten PDF-Objekte verarbeiten. Es wird dann ein hässlicher Fehler angezeigt. Eine Lösung hierfür ist, das Dokument per PDF-Druck ohne komprimierte Objekte zu konvertieren.

<?php
/*
    imposition of a DIN-A formated single page PDF (no compression!) into a DIN-A6 booklet
    step 1: load source pdf and check if page count can be divided by 4
    step 2: generate a "simple" imposition of the booklet
    step 3: place sheet of simple booklet onto DIN-A4

    uses mpdf v8+
*/
require_once(__DIR__ . '/vendor/autoload.php');

use \Mpdf\Mpdf;
use setasign\Fpdi\PdfParser\StreamReader;

$importFile = __DIR__ .'/PATH/TO/MY-ORIGINAL-DOCUMENT.pdf';

// landscape DIN-A4
$config = [
    'pageWidth' => 297,
    'pageHeight' => 210,
];

$mpdfConfig = [
    'format' => [$config['pageWidth'], $config['pageHeight']],
    'margin_left' => 0,
    'margin_right' => 0,
    'margin_top' => 0,
    'margin_bottom' => 0,
    'mode' => 'utf-8',
];

$mpdf = new Mpdf($mpdfConfig);

// get number of pages from original file and set original sile
$pageCount = $mpdf->setSourceFile($importFile);

// check if there are enough pages to form a booklet
if ($pageCount % 4) {
    die('insufficient amount of pages! the page count must be divided by 4!');
}

// get the number of paper sheet sides to print the booklet
$sheetCount = $pageCount / 2;

// walk through all paper sheets (front and back side)
for ($i = 1; $i <= $sheetCount; $i++) {
    if ($i % 2) { // ood sheets
        // left half
        placePage($pageCount - $i + 1, 1);        
        // right half
        placePage($i, 2);       
    } else { // even sheet      
        // left half
        placePage($i, 1);
        placePage($pageCount - $i + 1, 2);
    }  
    if ($i != $sheetCount) $mpdf->AddPage();
}

// save output as string
$simpleLayout = $mpdf->Output('dump.pdf', 'S');

// start over again
unset($mpdf);

// rotate format
$config = [
    'pageWidth' => 210,
    'pageHeight' => 297,
];
$mpdfConfig['format'] = [$config['pageWidth'], $config['pageHeight']];

$mpdf = new Mpdf($mpdfConfig);

// get the previously generated document
$stream = StreamReader::createByString($simpleLayout);
$pageCount = $mpdf->setSourceFile($stream);

$sheetCount = $pageCount / 2;

// cut line styling
$mpdf->SetDrawColor(228, 228, 228);
$mpdf->SetLineWidth(0.1);

for ($i = 1; $i <= $sheetCount+1; $i++) {
    $needNewPage = false;
    if ($i % 2) { // ood sheet (1, 3, 5, ...)
        // upper half
        $currentPage = ($i * 2) - 1;
        if ($currentPage <= $pageCount) { 
            placeSheet($currentPage, 1);
            $needNewPage = true;  // after an ood sheet there needs to be a new page
        }        
        // lower half
        $currentPage = $currentPage + 2;
        if ($currentPage <= $pageCount) {
            placeSheet($currentPage, 2);
        }
    } else { // even sheet (2, 4, 6, ..)
        // upper half
        $currentPage = ($i * 2) - 2;
        if ($currentPage <= $pageCount) {
            placeSheet($currentPage, 1);
        }
        // lower half
        $currentPage = $currentPage + 2;
        if ($currentPage <= $pageCount) {
            placeSheet($currentPage, 2);
            $needNewPage = true;
            if ($currentPage + 1 > $pageCount) $needNewPage = false; // new page for following sheet
        } 
    }
    // draw cut line
    $mpdf->Line(0, $config['pageHeight'] / 2, $config['pageWidth'], $config['pageHeight'] / 2);

    if ($needNewPage) $mpdf->AddPage();
}

$mpdf->Output();

function placePage($pageNumber, $position = 1) {
    global $config, $mpdf;  
    $tplId = $mpdf->importPage($pageNumber);
    switch ($position) {
        case 1: // left
            $mpdf->useTemplate($tplId, 0, 0, $config['pageWidth'] / 2, $config['pageHeight']);
        break;
        case 2: // right
            $mpdf->useTemplate($tplId, $config['pageWidth'] / 2, 0, $config['pageWidth'] / 2, $config['pageHeight']);
        break;  
    }
}

function placeSheet($pageNumber, $position = 1) {
    global $config, $mpdf;
    $tplId = $mpdf->importPage($pageNumber);
    switch ($position) {
        case 1: // upper half
            $mpdf->useTemplate($tplId, 0, 0, $config['pageWidth'], $config['pageHeight'] / 2);
        break;
        case 2: // lower half
            $mpdf->useTemplate($tplId, 0, $config['pageHeight'] / 2, $config['pageWidth'], $config['pageHeight'] / 2);
        break;
    }    
}

Nachtrag: Knoten im Kopf gelöst

Dieser unflexible Lösungsansatz hat mir keine Ruhe gelassen. Der Clou an der Sache ist der Algorithmus zur Platzierung der Seiten auf den Bögen.

Hierzu folgende Skizze:

Skizze für den kompletten Algorithmus

Obacht gilt, wenn die Gesamtseitenzahl der Vorlage nicht ohne Rest durch 8 teilbar ist, dann verschiebt sich die Platzierung auf den Seiten des letzten Bogens.

Der folgende Code erstellt das o.g. Ergebnis ohne Zwischenschritte direkt:

<?php
/*
    imposition of a DIN-A formated single page PDF (no compression!) into a DIN-A6 booklet
    step 1: load source pdf and check if page count can be divided by 4
    step 2: generate a "simple" imposition of the booklet
    step 3: place sheet of simple booklet onto DIN-A4

    uses mpdf v8+
*/
require_once(__DIR__ . '/vendor/autoload.php');

use \Mpdf\Mpdf;
use setasign\Fpdi\PdfParser\StreamReader;

$importFile = __DIR__ .'/PATH/TO/MY-ORIGINAL-DOCUMENT.pdf';

// landscape DIN-A4
$config = [
    'pageWidth' => 210,
    'pageHeight' => 297,
];

$mpdfConfig = [
    'format' => [$config['pageWidth'], $config['pageHeight']],
    'margin_left' => 0,
    'margin_right' => 0,
    'margin_top' => 0,
    'margin_bottom' => 0,
    'mode' => 'utf-8',
];

$mpdf = new Mpdf($mpdfConfig);

// get number of pages from original file and set original sile
$pageCount = $mpdf->setSourceFile($importFile);

// check if there are enough pages to form a booklet
if ($pageCount % 4) {
    die('insufficient amount of pages! the page count must be divided by 4!');
}

// cut line styling
$mpdf->SetDrawColor(228, 228, 228);
$mpdf->SetLineWidth(0.1);

// get the number of paper sheet sides to print the booklet
$sheetCount = $pageCount / 4;
$needExtraSheet = ($pageCount % 8) ? true : false;;

// walk through all paper sheets (front and back side)
for ($i = 1; $i <= $sheetCount; $i++) {

    if ($i % 2) { // ood sheets 
        placePage($pageCount - 2 * $i  + 2, 1); // top left     
        placePage(($i * 2) - 1, 2); // top right        
        if ($i == $sheetCount AND $needExtraSheet) { // add a page at the end and place final pages         
            $mpdf->Line(0, $config['pageHeight'] / 2, $config['pageWidth'], $config['pageHeight'] / 2); // draw cut line
            $mpdf->AddPage();
            placePage($i * 2, 1); // top left       
            placePage($pageCount - 2 * $i + 1, 2); // top right         
        } else { // normal behavior 
            placePage($pageCount - 2 * $i, 3); // bottom left
            placePage(($i * 2) + 1 ,4); // bottom right 
        }       
    } else { // even sheet  
        placePage($i * 2 - 2, 1); // top left       
        placePage($pageCount - 2 * $i + 3, 2); // top right
        placePage($i * 2, 3); // bottom left
        placePage($pageCount - 2 * $i + 1 ,4); // bottom right
    }  
    $mpdf->Line(0, $config['pageHeight'] / 2, $config['pageWidth'], $config['pageHeight'] / 2); // draw cut line
    if ($i != $sheetCount) $mpdf->AddPage();
}

$mpdf->Output();

function placePage($pageNumber, $position = 1) {
    global $config, $mpdf, $pageCount;
    $tplId = $mpdf->importPage($pageNumber);
    switch ($position) {
        case 1: // top right
            $mpdf->useTemplate($tplId, 0, 0, $config['pageWidth'] / 2, $config['pageHeight'] / 2);
        break;
        case 2: // top left
            $mpdf->useTemplate($tplId, $config['pageWidth'] / 2, 0, $config['pageWidth'] / 2, $config['pageHeight'] / 2);
        break;
        case 3: //bottom right
            $mpdf->useTemplate($tplId, 0, $config['pageHeight'] / 2, $config['pageWidth'] / 2, $config['pageHeight'] / 2);      
        break;
        case 4: //bottom left
            $mpdf->useTemplate($tplId, $config['pageWidth'] / 2, $config['pageHeight'] / 2, $config['pageWidth'] / 2, $config['pageHeight'] / 2);
        break;
    }
}