Das Aufstempeln eines Wasserzeichens oder eines Logos auf jede Seite eines Dokuments sieht nach einer Fünf-Minuten-Aufgabe aus, bis Sie das Ergebnis in einem Dateigrößen-Inspektor öffnen. Der offensichtliche Ansatz besteht darin, die Seiten durchzugehen und auf jeder Seite dieselben Text- oder Bildobjekte erneut zu erstellen. Das funktioniert visuell, ist aber auf eine Weise verschwenderisch, die sich summiert. Ein diagonales "ENTWURF"-Wasserzeichen (DRAFT), das direkt auf einen hundertseitigen Bericht gezeichnet wird, bedeutet hundert Kopien derselben Pfad- und Textdaten in den Inhaltsströmen, und die gespeicherte Datei trägt jede einzelne davon in sich
Ein Form XObject ist das Konstrukt, das PDF bereitstellt, um genau dies zu vermeiden. Es hüllt ein Stück wiederverwendbaren Inhalts, eine ganze Seite oder eine kleine Vorlage, in ein einziges benanntes Objekt, das viele Male an vielen Positionen gezeichnet werden kann. Der Inhalt existiert in der Datei nur einmal. Jede Seite, die den Stempel haben möchte, enthält eine kurze Anweisung, die besagt: "Zeichne XObject N hier, mit dieser Transformation." Ein hundertseitiges Wasserzeichen fügt der Datei dann ein Inhaltsobjekt anstelle von hundert hinzu, und das ist der Unterschied zwischen einem Dokument, das linear mit seiner Seitenzahl wächst, und einem, das dies nicht tut. Wasserzeichen, Logostempel, Seitenzahlvorlagen und Siegel stellen alle dasselbe Problem dar, und das Form XObject ist das richtige Werkzeug für jedes einzelne davon
Warum ein gespeichertes Objekt hundert Neuzeichnungen übertrifft
Die Einsparung ist strukturell, nicht kosmetisch. Eine PDF-Seite wird durch Ausführen ihres Inhaltsstroms (Content Stream), einer Abfolge von Zeichenoperatoren, gerendert. Wenn Sie einen Stempel pro Seite neu zeichnen, hängen Sie die vollständige Operatorsequenz für diesen Stempel an den Strom jeder Seite an, und die Bytes werden so oft dupliziert, wie Sie Seiten haben. Ein Form XObject verschiebt diese Operatoren in einen einzigen Strom, der einmal im Dokument gespeichert wird. Die Referenz, die eine einzelne Seite behält, ist klein: Sie schiebt (pushes) eine Transformationsmatrix, ruft das XObject auf und stellt den Zustand (State) wieder her. Die Seitenzahl vervielfacht nicht mehr die Kosten für das Bildmaterial
Dies ist besonders wichtig, wenn der Stempel schwer ist. Ein Vektorsiegel mit Hunderten von Pfadsegmenten oder eine Logo-Bitmap ist speicherintensiv. Wenn es einmal gespeichert und referenziert wird, wird der schwere Teil nur einmal bezahlt, und der Overhead pro Seite besteht aus ein paar Bytes für den Aufruf. Das visuelle Ergebnis auf der Seite ist identisch mit einer direkten Neuzeichnung, was der springende Punkt ist. Der Leser kann den Unterschied nicht erkennen; die Dateigröße jedoch sehr wohl
Erfassen einer Seite in ein XObject
PDFium erstellt das wiederverwendbare Objekt aus einer bestehenden Seite. Die Quelle ist eine Seite in einem von Ihnen geöffneten Dokument, eine kleine einseitige PDF-Datei, die nichts anderes als Ihr Wasserzeichen enthält, oder eine bestimmte Seite einer größeren Datei. CreateXObjectFromPage erfasst den Inhalt dieser Quellseite in ein wiederverwendbares Handle, das zum Zieldokument gehört, also zu dem Dokument, das Sie stempeln
var
Dest, Stamp: TPdf;
XObject: TPdfXObject;
begin
Dest := TPdf.Create(nil);
Stamp := TPdf.Create(nil);
try
Dest.FileName := 'Report.pdf';
Dest.Active := True;
Stamp.FileName := 'Watermark.pdf'; // one page of artwork
Stamp.Active := True;
if not (Dest.Active and Stamp.Active) then
raise Exception.Create('Could not open the input documents');
// Capture page 0 of the stamp document into a reusable handle that
// is owned by Dest. Source must be Active; the index is zero-based.
XObject := Dest.CreateXObjectFromPage(Stamp, 0);
if XObject = nil then
raise Exception.Create('Could not build the stamp XObject');
// ... place it, then free it before closing Stamp (see below) ...
Die Signatur lautet CreateXObjectFromPage(Source: TPdf; SourcePageIndex: Integer): TPdfXObject. Die Methode löst eine Ausnahme aus, wenn das Quelldokument nicht Active ist, und sie gibt nil zurück, anstatt eine Ausnahme auszulösen, wenn PDFium das Objekt nicht erstellen kann. Daher ist die obige explizite Prüfung nicht optional. Das zurückgegebene Handle ist ein TPdfXObject, das Ihnen gehört, und die beiden daran geknüpften Lebensdauerbeschränkungen (Lifetime Constraints) sind der Teil dieser ganzen Übung, der die Leute überrascht, weshalb sie unten einen eigenen Abschnitt erhalten
Platzieren des Stempels auf einer Seite
Ein erfasstes XObject tut von sich aus nichts. Damit es erscheint, fügen Sie eine Kopie davon auf der aktuellen Seite des Dokuments, die durch die 1-basierte Eigenschaft PageNumber ausgewählt wurde, mit InsertFormObjectFromXObject ein. Dieser Aufruf gibt das zugrunde liegende Seitenobjekt zurück, ein FPDF_PAGEOBJECT, und das zurückgegebene Handle ist die Art und Weise, wie Sie die Platzierung positionieren. Ohne eine Transformation landet der Stempel am Ursprung in den eigenen Koordinaten der Quellseite, was selten der Ort ist, an dem Sie ihn haben möchten
Da InsertFormObjectFromXObject pro Aufruf eine Kopie einfügt und jedes Mal ein frisches Seitenobjekt zurückgibt, können Sie dasselbe XObject mehrmals auf einer Seite mit unterschiedlichen Transformationen zeichnen, und der gespeicherte Inhalt wird in der Datei immer noch nur einmal gezählt. Ein Logo in der Ecke und ein schwaches ganzseitiges Wasserzeichen können aus demselben erfassten Objekt stammen
var
PageObj: FPDF_PAGEOBJECT;
M: TPdfMatrix;
RawM: FS_MATRIX;
begin
// The current page of Dest receives one copy of the XObject.
PageObj := Dest.InsertFormObjectFromXObject(XObject);
if PageObj = nil then
raise Exception.Create('Insert failed on this page');
// Position it: move 200 units right, 500 up, at 70% scale.
M := TPdfMatrix.Create;
try
M.Scale(0.7, 0.7);
M.Translate(200, 500);
RawM := M.Handle;
if FPDFPageObj_SetMatrix(PageObj, RawM) = 0 then
raise Exception.Create('Cannot assign the stamp matrix');
finally
M.Free;
end;
Dest.UpdatePage; // commit this page's edits to its content stream
// if not Dest.SaveAs(...) then ... when every page is done.
end;
Zwei Details zur Verwaltung machen dies sicher. Erstens: Sobald das Seitenobjekt eingefügt ist, gehört es zur Seite, nicht zum XObject. Die spätere Freigabe des XObject macht die bereits vorgenommenen Platzierungen nicht ungültig. Das ist es, was die unten beschriebene Reihenfolge von Erstellen-Platzieren-Freigeben funktionieren lässt. Zweitens: Das Einfügen und Positionieren ändert nur die Objektliste der Seite im Speicher; UpdatePage serialisiert diese Liste zurück in den Inhaltsstrom der Seite. Daher wird eine Seite, die Sie bearbeiten, ohne diesen Aufruf zu tätigen, so gespeichert, als wäre der Stempel nie platziert worden
Die Regel zur Lebensdauer von Handles, die Leuten zum Verhängnis wird
Zwei Beschränkungen regeln das XObject-Handle, und das Ignorieren beider führt zu einem Fehler, der nichts mit seiner Ursache zu tun zu haben scheint. Erstens muss das Quelldokument in dem Moment aktiv sein, in dem Sie CreateXObjectFromPage aufrufen. Die Erfassung liest den Inhalt der Quellseite aus dem aktiven (live) Quelldokument, sodass dieses Dokument und seine Seite geöffnet und gültig sein müssen, wenn das Handle erstellt wird. Zweitens, und dies ist diejenige, die die Leute überrascht, muss das Handle freigegeben werden, bevor die Quellseite geschlossen wird, und in der Praxis, bevor Sie das Quelldokument, aus dem es stammt, schließen oder freigeben
Der Grund dafür ist, dass das XObject eine Referenz auf eine Struktur ist, die das Quelldokument noch besitzt. Es handelt sich nicht um eine losgelöste, in sich geschlossene Kopie, die Sie nach dem Verschwinden der Quelle mit sich herumtragen können. Schließen Sie zuerst die Quelle, zeigt das Handle weiterhin auf abgebauten Inhalt. Eine spätere Freigabe oder jede andere Verwendung greift daher auf Speicher zu, der nicht mehr gültig ist. Das Symptom ist der Klassiker für ein baumelndes Handle (Dangling Handle): eine Zugriffsverletzung (Access Violation) beim Herunterfahren oder eine zeitweilige Beschädigung (Corruption), die je nach Zuordnungsreihenfolge wandert, mit einem Stack, der auf den Bereinigungscode (Cleanup Code) verweist und nicht auf die Zeile, die das Problem tatsächlich verursacht hat. Die Lösung liegt in der Reihenfolge, nicht im defensiven Programmieren. Erstellen Sie das XObject, fügen Sie es auf jeder Seite ein, die es benötigt, geben Sie das XObject frei und schließen Sie erst dann das Quelldokument. Der Destruktor von TPdfXObject gibt das zugrunde liegende PDFium-Handle für Sie frei, sodass die rechtzeitige Freigabe des Wrappers Ihre einzige Verantwortung ist
Die Matrix und was ihre sechs Zahlen bedeuten
Die Platzierung ist eine 2D-affine Transformation, dieselbe, die PDF überall zur Positionierung von Inhalten verwendet (ISO 32000-1, Abschnitt 8.3.4). Sie besteht aus sechs Zahlen, geschrieben a, b, c, d, e, f, und PDFium stellt sie als FS_MATRIX-Record zur Verfügung. Sie bilden einen Punkt aus dem eigenen Raum des Objekts auf den Seitenraum (Page Space) ab:
// x' = a*x + c*y + e
// y' = b*x + d*y + f
//
// a, d : horizontal and vertical scale
// b, c : the shear / rotation terms
// e, f : translation (where the origin lands on the page)
Sie können diese sechs Werte von Hand ausfüllen, aber beim manuellen Zusammensetzen geht die Rotation schief, da die Rotation alle vier Werte a, b, c, d miteinander vermischt. Der Wrapper TPdfMatrix aus der Unit FPdfMatrix setzt die gängigen Operationen für Sie zusammen und multipliziert sie nachträglich (post-multiplies), sodass Translate, Scale und Rotate in der Reihenfolge verkettet werden, in der Sie sie aufrufen. Ein diagonales Wasserzeichen ist eine Rotation gefolgt von einer Translation, um es neu zu zentrieren; ein Logo in der Ecke ist eine Skalierung gefolgt von einer Translation. Wenn die Matrix bereit ist, kopieren Sie ihren Rohwert, die Eigenschaft Handle vom Typ FS_MATRIX, in eine lokale Variable und übergeben diese an FPDFPageObj_SetMatrix; der Import deklariert die Matrix als var-Parameter, daher kann ihr nicht direkt eine Eigenschaft übergeben werden, und das Ergebnis ist im Fehlerfall 0. Das systemnähere FPDFPageObj_Transform, das die sechs Werte direkt als Doubles annimmt, ist verfügbar, wenn Sie lieber Zahlen übergeben als einen Wrapper zu erstellen
Jede Seite stempeln, in der richtigen Reihenfolge
Das vollständige Muster setzt die Teile in der Reihenfolge zusammen, die die Regel zur Lebensdauer verlangt. Öffnen Sie beide Dokumente, erfassen Sie den Stempel einmal, gehen Sie die Zielseiten durch, indem Sie nacheinander die 1-basierte PageNumber festlegen und eine Kopie einfügen sowie positionieren, committen Sie jede Seite mit UpdatePage, geben Sie dann das XObject frei, speichern Sie mit SaveAs und lassen Sie das Quelldokument zuletzt schließen
procedure StampEveryPage(const ASource, AStamp, AOutput: string);
var
Dest, Stamp: TPdf;
XObject: TPdfXObject;
PageObj: FPDF_PAGEOBJECT;
M: TPdfMatrix;
RawM: FS_MATRIX;
I: Integer;
begin
Dest := TPdf.Create(nil);
Stamp := TPdf.Create(nil);
try
Dest.FileName := ASource;
Dest.Active := True;
Stamp.FileName := AStamp;
Stamp.Active := True;
if not (Dest.Active and Stamp.Active) then
raise Exception.Create('Could not open the input documents');
// 1. Capture the artwork once. Stamp is Active here.
XObject := Dest.CreateXObjectFromPage(Stamp, 0);
if XObject = nil then
raise Exception.Create('Could not capture the stamp page');
try
// 2. Place a copy on every page of Dest. PageNumber is 1-based.
for I := 1 to Dest.PageCount do
begin
Dest.PageNumber := I; // make page I current
PageObj := Dest.InsertFormObjectFromXObject(XObject);
if PageObj = nil then
Continue;
M := TPdfMatrix.Create;
try
M.Rotate(45); // diagonal watermark
M.Translate(150, 100); // nudge into position
RawM := M.Handle;
FPDFPageObj_SetMatrix(PageObj, RawM);
finally
M.Free;
end;
Dest.UpdatePage; // commit this page's edits
end;
finally
XObject.Free; // 3. free BEFORE Stamp closes
end;
// 4. Write the result while Dest is still open.
if not Dest.SaveAs(AOutput) then
raise Exception.Create('Could not save ' + AOutput);
finally
Stamp.Free; // source closes last
Dest.Free;
end;
end;
Die Form der try-Blöcke leistet die eigentliche Arbeit. Das innere finally gibt das XObject frei, bevor die Kontrolle jemals das äußere finally erreichen kann, das Stamp freigibt. Das Handle wird also immer freigegeben, solange seine Quelle noch lebt, selbst wenn mitten in der Schleife eine Ausnahme ausgelöst wird. Machen Sie diese Verschachtelung (Nesting) richtig, und die Regel zur Lebensdauer kümmert sich um sich selbst
Das Stempeln ist ein Eckpfeiler eines größeren Toolkits zum Erstellen und Bearbeiten von Seiteninhalten. Wenn Ihr Stempel selbst ein Bild und keine erfasste Seite ist, behandelt Konvertieren von Bildern in PDF-Dokumente mit PDFium zunächst, wie Sie diese Bitmap in ein Dokument bekommen. Und wenn das, was Sie neben dem sichtbaren Stempel mitführen möchten, eine Datei und keine Tinte auf der Seite ist, zeigt Arbeiten mit PDF-Anhängen in Delphi die Seite mit den eingebetteten Dateien. All dies wird mit der PDFium-Komponente für Delphi und C++Builder geliefert, zusammen mit den Rendering-, Bearbeitungs- und Dokumenten-APIs, die an anderer Stelle in diesem Blog behandelt werden