Technical Article

N-up-Imposition und Seitenneuordnung mit PDFium

Zusammenführen (Merge) und Aufteilen (Split) sind die beiden Seitenoperationen, nach denen jeder zuerst greift, und sie decken viel ab. Sie decken jedoch nicht alles ab. Es gibt eine separate Familie von Aufgaben, die Seiten neu anordnet, anstatt ganze Dateien zu verschieben: das Verteilen von vier Folien auf ein Blatt für ein Handout, das Ziehen einer Seite vom Ende eines Dokuments an den Anfang oder das Extrahieren der Seiten 3, 7 und 12 in einen kurzen Auszug, ohne den Rest zu berühren. PDFium stellt genau hierfür drei Methoden bereit, und jede verhält sich anders als das Zusammenführen und Aufteilen, das Sie bereits kennen. Dieser Artikel beschreibt, was sie tun, wo die Ausgabepunkte liegen und ein Besitzdetail, das in der Praxis zu einem Absturz geführt hat

Die drei sind ImportNPagesToOne für die N-up-Imposition, MovePages für die Neuordnung an Ort und Stelle und ImportPagesByIndex für die Extraktion von Teilmengen. Das Zusammenführen stapelt Dokumente Ende an Ende und belässt die Seitenzahl gleich der Summe der Eingaben. Das Aufteilen schreibt mehrere Ausgabedateien aus einer Eingabe. Die drei Operationen hier liegen dazwischen: Eine davon ändert, wie viele Quellseiten sich ein Blatt teilen, eine ändert die Reihenfolge innerhalb eines einzelnen Dokuments und eine kopiert eine ausgewählte Handvoll Seiten in ein anderes Dokument. Zu wissen, welche welche ist, erspart Ihnen einen umständlichen Tanz aus Zusammenführen und Löschen, wo ein einziger Aufruf genügen würde

Was N-up-Imposition tatsächlich tut

Ausschießung (Imposition) ist der Begriff aus der Druckvorstufe für das Anordnen mehrerer Quellseiten auf einem größeren Bogen, sodass das gedruckte und gefaltete Ergebnis in der richtigen Reihenfolge lesbar ist. Die alltägliche Variante ist das 2-up-Handout, die 4-up-Broschürensignatur oder der Kontaktabzug, bei dem ein Dutzend Miniaturansichten auf eine Seite passen. PDFium steuert die Geometrie über einen Aufruf

function ImportNPagesToOne(
  OutputWidth, OutputHeight: Single;
  NumX, NumY               : Cardinal): TPdf;

NumX und NumY beschreiben das Raster. A Wert von 2, 1 platziert zwei Quellseiten nebeneinander; 2, 2 packt vier in ein Quadrantenlayout; 4, 3 erstellt einen 12-up-Kontaktabzug. PDFium liest die Quellseiten der Reihe nach, skaliert jede einzelne herunter, damit sie in ihre Zelle passt, und füllt das Raster von links nach rechts und von oben nach unten. Es beginnt ein neues Ausgabeblatt, sobald das aktuelle Raster voll ist. Die Quellseiten werden nicht verändert. Was Sie zurückerhalten, ist ein neues Dokument, dessen Seiten Komposite sind

Die Ausgabegröße wird in Punkten angegeben, nicht in Pixeln

OutputWidth und OutputHeight sind PDF-Benutzereinheiten, und eine PDF-Benutzereinheit ist ein Punkt, was einem Zweiundsiebzigstel Zoll entspricht. Die Einheit deklariert die physische Größe des Ausgabeblatts und hat nichts mit Bildschirmpixeln oder der Render-DPI zu tun. Dies ist the häufigste Fehler beim Ausschieben, da ein an Bitmaps gewöhnter Entwickler nach einer Pixelanzahl greift und am Ende ein Blatt im Format einer Briefmarke oder einer Plakatwand erhält

Die Zahlen, die man sich merken sollte, sind die beiden am häufigsten verwendeten Seitengrößen. US-Letter ist 612 mal 792 Punkte groß (8,5 Zoll mal 72 ist 612, und 11 Zoll mal 72 ist 792). A4 ist aufgrund seiner Abmessungen von 210 mal 297 Millimetern etwa 595 mal 842 Punkte groß. Der eigene Header des Bindings drückt die Regel klar aus, dass eine Einheit einem Zweiundsiebzigstel Zoll entspricht, und die Unit stellt eine PointsPerInch-Konstante bereit, die gleich 72 ist, falls Sie eine Größe lieber im Code aus Zoll berechnen möchten, anstatt den Literalwert zu schreiben

const
  LetterW = 612.0;   // 8.5 in * 72
  LetterH = 792.0;   // 11  in * 72
var
  Source, Composite: TPdf;
begin
  Source := TPdf.Create(nil);
  Composite := nil;
  try
    Source.FileName := 'slides.pdf';
    Source.Active := True;

    // Four source pages per Letter sheet, 2 by 2 grid.
    Composite := Source.ImportNPagesToOne(LetterW, LetterH, 2, 2);
    if Composite = nil then
      raise Exception.Create('PDFium rejected the imposition arguments');

    Composite.SaveAs('slides-4up.pdf');
  finally
    Composite.Free;   // see the next section: this is mandatory
    Source.Free;
  end;
end;

Das zurückgegebene Handle müssen Sie selbst freigeben

Lesen Sie die Signatur noch einmal. ImportNPagesToOne gibt ein TPdf zurück, kein Boolean. Dieser Rückgabewert ist ein brandneues Dokumenten-Handle, das separat von der Quelle zugewiesen wird, und der Aufrufer besitzt es. Das Quell-TPdf, auf dem Sie die Methode aufgerufen haben, bleibt unberührt und besitzt weiterhin sein eigenes Handle; das Komposit ist ein zweites, unabhängiges Objekt. Wenn Sie das zurückgegebene TPdf den Gültigkeitsbereich verlassen lassen, ohne es freizugeben, lecken Sie ein ganzes PDFium-Dokument

Der gefährlichere Fehler verläuft in die andere Richtung. Unter der Haube fordert die Methode über FPDF_ImportNPagesToOne ein neues FPDF_DOCUMENT von PDFium an und verpackt dieses rohe Handle im zurückgegebenen TPdf, sodass die Lebensdauer des Wrappers die des Handles bestimmt. Ab diesem Zeitpunkt gibt es genau einen Besitzer des Handles und genau eine Stelle, an der es geschlossen werden sollte: wenn Sie das zurückgegebene Objekt mit Free freigeben. Ein unvorsichtiger Fehlerpfad, der sowohl den Wrapper freigibt als auch FPDF_CloseDocument auf dem erfassten rohen Handle aufruft, schließt dasselbe PDFium-Dokument zweimal. Das ist eine doppelte Freigabe und genau der Fehler, der hier einmal aufgetreten ist. Die Regel, die dies verhindert, ist kurz. Schließen Sie das Dokument nur auf einem Pfad, indem Sie das TPdf freigeben, das Ihnen die Methode übergeben hat, und greifen Sie niemals am Wrapper vorbei, um das Handle zu schließen, das er bereits übernommen hat

Daraus ergeben sich zwei Folgerungen. Erstens gibt die Methode nil zurück, wenn PDFium die Argumente ablehnt (wie eine Null auf einer der Rasterachsen oder ein Speicherzuweisungsfehler), sodass eine nil-Prüfung hingehört, bevor Sie das Ergebnis anfassen. Zweitens initialisieren Sie Ihre Ausgabevariable vor dem try mit nil und geben sie im finally frei, wie es das obige Beispiel tut, damit ein Fehler auf halbem Weg Sie nicht mit der Freigabe einer undefinierten Referenz zurücklässt oder die Freigabe ganz überspringt

Neuordnen von Seiten ohne Umschreiben

Die Neuordnung ändert ein Dokument an Ort und Stelle. MovePages hebt eine Reihe von Seiten aus ihren aktuellen Positionen und legt sie an einem Ziel ab, wobei alles andere um den verschobenen Block herum verschoben wird, sodass die Seitenzahl gleich bleibt

function MovePages(
  const PageIndices: array of Integer;
  DestPageIndex    : Integer): Boolean;

Die Indizes sind nullbasiert. PageIndices listet die zu verschiebenden Seiten in der Reihenfolge auf, in der sie enden sollen, und DestPageIndex ist der Index, auf dem die erste verschobene Seite landet, nachdem sich die Verschiebung eingependelt hat. Da PDFium die Seiten neu positioniert, anstatt ihren Inhalt zu kopieren und neu zu komprimieren, ist die Operation kostengünstig und verlustfrei: Die Seitenobjekte behalten ihre Streams, ihre Ressourcen und ihre Qualität. Dies ist der Aufruf hinter einem Seitenselektor mit Drag-and-Drop, bei dem ein Benutzer ein Vorschaubild in einen neuen Slot zieht und Sie die neue Reihenfolge mit einer einzigen Verschiebung festlegen. Er gibt False zurück, wenn ein Index außerhalb des gültigen Bereichs liegt, prüfen Sie also das Ergebnis, anstatt davon auszugehen, dass die Neuordnung erfolgreich war

var
  Doc: TPdf;
begin
  Doc := TPdf.Create(nil);
  try
    Doc.FileName := 'report.pdf';
    Doc.Active := True;

    // Move the last page (index 4 in a 5-page file) to the very front.
    if not Doc.MovePages([4], 0) then
      raise Exception.Create('MovePages rejected the index');

    Doc.SaveAs('report-reordered.pdf');
  finally
    Doc.Free;
  end;
end;

Extrahieren einer Teilmenge über den Index

Die dritte Operation kopiert eine explizite Gruppe von Seiten aus einem Dokument in ein anderes. ImportPagesByIndex nimmt das Quelldokument und ein nullbasiertes Index-Array und fügt diese Seiten an einer ausgewählten Position in das Ziel ein

function ImportPagesByIndex(
  Source           : TPdf;
  const PageIndices: array of Integer;
  InsertAt         : Integer= 0): Boolean;

Sie rufen sie auf dem Zieldokument auf und übergeben die Quelle als erstes Argument. PageIndices nennt die zu ziehenden Quellseiten in der von Ihnen gewünschten Reihenfolge; InsertAt ist der nullbasierte Slot im Ziel, in den die erste importierte Seite eingefügt wird, sodass 0 sie vor der vorhandenen ersten Seite platziert und die aktuelle Seitenzahl des Ziels angehängt wird. Ein leeres Array importiert jede Seite, was den Aufruf zu einer vollständigen Kopie macht, falls Sie eine benötigen. Er gibt False zurück, wenn ein Index in der Quelle außerhalb des gültigen Bereichs liegt

Hier ist der Unterschied zum Aufteilen (Split) wichtig. Das Aufteilen schreibt separate Dateien, wobei eine Operation viele Ausgaben auf der Festplatte erzeugt. ImportPagesByIndex macht genau das Gegenteil: Es sammelt eine ausgewählte Gruppe von Seiten in einem einzigen Zieldokument im Speicher, das Sie dann einmal speichern. Wenn die Aufgabe lautet „Gib mir die Seiten 3, 7 und 12 als ein kurzes PDF", ist dies der direkte Weg, und es kapselt FPDF_ImportPagesByIndex unter der Haube

var
  Source, Excerpt: TPdf;
begin
  Source := TPdf.Create(nil);
  Excerpt := TPdf.Create(nil);
  try
    Source.FileName := 'manual.pdf';
    Source.Active := True;
    Excerpt.CreateDocument;   // start an empty target

    // Pull pages 3, 7 and 12 (zero-based 2, 6, 11) into the excerpt.
    if not Excerpt.ImportPagesByIndex(Source, [2, 6, 11], 0) then
      raise Exception.Create('A requested page index is out of range');

    Excerpt.SaveAs('manual-excerpt.pdf');
  finally
    Excerpt.Free;
    Source.Free;
  end;
end;

Sauberes Zusammenführen

Der Ablauf ist bei allen drei Methoden im Grunde gleich: Öffnen Sie die Quelle, indem Sie FileName setzen und Active auf True stellen, führen Sie die Operation aus, speichern Sie mit SaveAs und geben Sie frei, was Sie besitzen. Der einzige Punkt, der Aufmerksamkeit erfordert, ist die Frage, welche Aufrufe ein neues Dokument zuweisen. MovePages verändert das Dokument, das Sie bereits halten, sodass es nur ein Objekt freizugeben gilt. ImportPagesByIndex schreibt in ein von Ihnen selbst erstelltes Ziel, sodass Sie die Quelle und das geöffnete Ziel freigeben. ImportNPagesToOne ist die Ausnahme, da das neue Dokument der Rückgabewert der Methode ist und nicht etwas, das Sie selbst konstruiert haben. Zu vergessen, dass es sich um ein separates, dem Aufrufer gehörendes Handle handelt, ist die Ursache für Speicherlecks und doppeltes Freigeben. Initialisieren Sie das Ergebnis mit nil, prüfen Sie es nach dem Aufruf und geben Sie es auf einem einzigen Pfad frei

Wenn Ihre eigentliche Aufgabe darin besteht, ganze Dateien zu kombinieren, anstatt Seiten neu anzuordnen, lesen Sie unter Zusammenführen mehrerer PDF-Dateien in ein Dokument nach. Wenn es umgekehrt darum geht, ein Dokument in mehrere Dateien aufzuteilen, lesen Sie Aufteilen von PDF-Dokumenten in mehrere Dateien. Die hier beschriebenen Impositions- und Neuordnungsmethoden werden als Teil der PDFium Component für Delphi und C++Builder zusammen mit den APIs zum Laden, Rendern und Bearbeiten ausgeliefert, die an anderer Stelle auf diesem Blog behandelt werden