Η τοποθέτηση ενός υδατογραφήματος ή ενός λογότυπου σε κάθε σελίδα ενός εγγράφου φαίνεται σαν δουλειά πέντε λεπτών, μέχρι να ανοίξετε το αποτέλεσμα σε έναν επιθεωρητή μεγέθους αρχείου. Η προφανής προσέγγιση είναι να περιηγηθείτε στις σελίδες και, σε καθεμία, να δημιουργήσετε ξανά τα ίδια αντικείμενα κειμένου ή εικόνας. Αυτό λειτουργεί οπτικά, αλλά είναι σπάταλο με τρόπο που συσσωρεύεται. Ένα διαγώνιο υδατογράφημα "DRAFT" σχεδιασμένο απευθείας σε μια αναφορά εκατό σελίδων είναι εκατό αντίγραφα των ίδιων δεδομένων διαδρομής και κειμένου που κάθονται στις ροές περιεχομένου, και το αποθηκευμένο αρχείο φέρει καθένα από αυτά.
Ένα Form XObject είναι η κατασκευή που παρέχει το PDF για να αποφευχθεί ακριβώς αυτό. Τυλίγει ένα κομμάτι επαναχρησιμοποιήσιμου περιεχομένου, μια ολόκληρη σελίδα ή ένα μικρό πρότυπο, σε ένα ενιαίο ονομαστικό αντικείμενο που μπορεί να βαφτεί πολλές φορές σε πολλές θέσεις. Το περιεχόμενο ζει στο αρχείο μία φορά. Και κάθε σελίδα που θέλει το stamp περιέχει μια σύντομη οδηγία που λέει "βάψε το XObject N εδώ, με αυτόν τον μετασχηματισμό". Ένα υδατογράφημα εκατό σελίδων προσθέτει τότε ένα αντικείμενο περιεχομένου στο αρχείο αντί για εκατό, και αυτή είναι η διαφορά ανάμεσα σε ένα έγγραφο που μεγαλώνει γραμμικά με τον αριθμό των σελίδων του και σε ένα που δεν μεγαλώνει. Τα υδατογραφήματα, τα stamps λογοτύπων, τα πρότυπα αριθμού σελίδας και οι σφραγίδες είναι όλα το ίδιο σχήμα προβλήματος, και το Form XObject είναι το σωστό εργαλείο για καθένα από αυτά.
Γιατί ένα αποθηκευμένο αντικείμενο κερδίζει εκατό επανασχεδιάσεις
Η εξοικονόμηση είναι δομική, όχι κοσμητική. Μια σελίδα PDF αποδίδεται εκτελώντας τη ροή περιεχομένου της (content stream), μια ακολουθία σχεδιαστικών τελεστών. Όταν επανασχεδιάζετε ένα stamp ανά σελίδα, προσαρτάτε την πλήρη ακολουθία τελεστών για αυτό το stamp στη ροή κάθε σελίδας, και τα byte διπλασιάζονται τόσες φορές όσες είναι οι σελίδες σας. Ένα Form XObject μεταφέρει αυτούς τους τελεστές σε μία ροή που αποθηκεύεται μία φορά στο έγγραφο. Η αναφορά που διατηρεί μια μεμονωμένη σελίδα είναι μικρή: προωθεί έναν πίνακα μετασχηματισμού (transformation matrix), καλεί το XObject και επαναφέρει την κατάσταση. Ο αριθμός των σελίδων δεν πολλαπλασιάζει πλέον το κόστος του δημιουργικού.
Αυτό έχει μεγαλύτερη σημασία όταν το stamp είναι βαρύ. Μια διανυσματική σφραγίδα με εκατοντάδες τμήματα διαδρομής, ή ένα bitmap λογοτύπου, είναι ακριβώς στην αποθήκευση. Όταν αποθηκεύεται μία φορά και αναφέρεται, το βαρύ μέρος πληρώνεται μία μόνο φορά και η επιβάρυνση ανά σελίδα είναι λίγα byte κλήσης. Το οπτικό αποτέλεσμα στη σελίδα είναι πανομοιότυπο με μια απευθείας επανασχεδίαση, που είναι και το ζητούμενο. Ο αναγνώστης δεν μπορεί να καταλάβει τη διαφορά. Το μέγεθος του αρχείου μπορεί σίγουρα.
Καγραφή μιας σελίδας σε ένα XObject
Το PDFium δημιουργεί το επαναχρησιμοποιήσιμο αντικείμενο από μια υπάρχουσα σελίδα. Η πηγή είναι μια σελίδα σε κάποιο έγγραφο που έχετε ανοιχτό, ένα μικρό μονοσέλιδο PDF που δεν περιέχει τίποτα άλλο εκτός από το δημιουργικό του υδατογραφήματος, ή μια συγκεκριμένη σελίδα ενός μεγαλύτερου αρχείου. Η CreateXObjectFromPage καταγράφει το περιεχόμενο αυτής της σελίδας-πηγής σε μια επαναχρησιμοποιήσιμη λαβή (handle) που ανήκει στο έγγραφο προορισμού, αυτό που σφραγίζετε.
var
Dest, Stamp: TPdf;
XObject: TPdfXObject;
begin
Dest := TPdf.Create;
Stamp := TPdf.Create;
try
Dest.LoadFromFile('Report.pdf');
Stamp.LoadFromFile('Watermark.pdf'); // one page of artwork
// 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) ...
Η υπογραφή είναι CreateXObjectFromPage(Source: TPdf; SourcePageIndex: Integer): TPdfXObject. Η μέθοδος επιστρέφει nil σε περίπτωση αποτυχίας αντί να εγείρει σφάλμα, επομένως ο ρητός έλεγχος παραπάνω δεν είναι προαιρετικός. Η λαβή που επιστρέφεται είναι ένα TPdfXObject που σας ανήκει, και οι δύο περιορισμοί διάρκειας ζωής που συνδέονται με αυτό είναι το μέρος αυτής της άσκησης που μπερδεύει τους χρήστες, γι' αυτό και έχουν τη δική τους ενότητα παρακάτω.
Τοποθέτηση του stamp σε μια σελίδα
Ένα καταγεγραμμένο XObject δεν κάνει τίποτα από μόνο του. Για να εμφανιστεί, εισάγετε ένα αντίγραφό του στην τρέχουσα σελίδα του εγγράφου με την InsertFormObjectFromXObject. Αυτή η κλήση επιστρέφει το υποκείμενο αντικείμενο σελίδας, ένα FPDF_PAGEOBJECT, και η λαβή που επιστρέφεται είναι ο τρόπος με τον οποίο τοποθετείτε το stamp. Χωρίς μετασχηματισμό, το stamp προσγειώνεται στην αρχή των συντεταγμένων της ίδιας της σελίδας-πηγής, κάτι που σπάνια είναι εκεί που θέλετε.
Επειδή η InsertFormObjectFromXObject εισάγει ένα αντίγραφο ανά κλήση και επιστρέφει ένα νέο αντικείμενο σελίδας κάθε φορά, μπορείτε να βάψετε το ίδιο XObject πολλές φορές σε μία σελίδα με διαφορετικούς μετασχηματισμούς, και το αποθηκευμένο περιεχόμενο εξακολουθεί να προσμετράται μία φορά στο αρχείο. Ένα λογότυπο γωνίας και ένα αχνό υδατογράφημα πλήρους σελίδας μπορούν να προέρχονται από το ίδιο καταγεγραμμένο αντικείμενο.
var
PageObj: FPDF_PAGEOBJECT;
M: TPdfMatrix;
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);
FPDFPageObj_SetMatrix(PageObj, M.Handle);
finally
M.Free;
end;
// Dest.SaveLoadedDocument(...) when every page is done.
end;
Μια λεπτομέρεια ιδιοκτησίας καθιστά την εκκαθάριση ασφαλή. Μόλις εισαχθεί, το αντικείμενο σελίδας ανήκει στη σελίδα, όχι στο XObject. Η απελευθέρωση του XObject αργότερα δεν ακυρώνει τις τοποθετήσεις που έχετε ήδη κάνει. Αυτό είναι που επιτρέπει στη σειρά create-place-free που περιγράφεται παρακάτω να λειτουργήσει.
Ο κανόνας διάρκειας ζωής της λαβής που μπερδεύει τους χρήστες
Δύο περιορισμοί διέπουν τη λαβή XObject, και η παράβλεψη οποιουδήποτε από τους δύο παράγει μια αποτυχία που φαίνεται άσχετη με την αιτία της. Πρώτον, το έγγραφο-πηγή πρέπει να είναι ενεργό τη στιγμή που καλείτε την CreateXObjectFromPage. Η καταγραφή διαβάζει το περιεχόμενο της σελίδας-πηγής από το ζωντανό έγγραφο-πηγή, επομένως αυτό το έγγραφο και η σελίδα του πρέπει να είναι ανοιχτά και έγκυρα όταν κατασκευάζεται η λαβή. Δεύτερον, και αυτό είναι που εκπλήσσει τους χρήστες, η λαβή πρέπει να απελευθερωθεί πριν κλείσει η σελίδα-πηγή, και στην πράξη πριν κλείσετε ή απελευθερώσετε το έγγραφο-πηγή από το οποίο προήλθε.
Ο λόγος είναι ότι το XObject είναι μια αναφορά σε δομή που το έγγραφο-πηγή εξακολουθεί να κατέχει. Δεν είναι ένα αποσπασμένο, αυτόνομο αντίγραφο που μπορείτε να μεταφέρετε αφού η πηγή έχει χαθεί. Κλείστε πρώτα την πηγή και η λαβή μένει να δείχνει σε περιεχόμενο που έχει καταστραφεί, επομένως η απελευθέρωσή της αργότερα, ή οποιαδήποτε άλλη χρήση της, λειτουργεί σε μνήμη που δεν είναι πλέον έγκυρη. Το σύμπτωμα είναι το κλασικό για μια λαβή που κρέμεται (dangling handle): ένα access violation κατά τον τερματισμό, ή περιοδική καταστροφή που μετακινείται ανάλογα με τη σειρά κατανομής, με ένα stack που δείχνει σε κώδικα εκκαθάρισης και όχι στη γραμμή που πραγματικά προκάλεσε το πρόβλημα. Η διόρθωση είναι η σειρά εκτέλεσης, όχι ο αμυντικός κώδικας. Δημιουργήστε το XObject, εισαγάγετε το σε κάθε σελίδα που το χρειάζεται, απελευθερώστε το XObject, και μόνο τότε κλείστε το έγγραφο-πηγή. Ο καταστροφέας (destructor) του TPdfXObject απελευθερώνει την υποκείμενη λαβή PDFium για εσάς, επομένως η απελευθέρωση του wrapper την κατάλληλη στιγμή είναι όλη η ευθύνη σας.
Ο πίνακας και τι σημαίνουν οι έξι αριθμοί του
Η τοποθέτηση είναι ένας δισδιάστατος συγγενικός μετασχηματισμός (2D affine transform), ο ίδιος που χρησιμοποιεί το PDF παντού για την τοποθέτηση περιεχομένου (ISO 32000-1, ενότητα 8.3.4). Πρόκειται για έξι αριθμούς, γραμμένους ως a, b, c, d, e, f, και το PDFium τους εκθέτει ως εγγραφή FS_MATRIX. Αντιστοιχίζουν ένα σημείο από τον δικό του χώρο του αντικειμένου στο χώρο της σελίδας:
// 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)
Μπορείτε να συμπληρώσετε αυτές τις έξι τιμές με το χέρι, αλλά η σύνθεσή τους με το χέρι είναι το σημείο όπου η περιστροφή (rotation) πάει λάθος, επειδή η περιστροφή αναμιγνύει και τα τέσσερα a, b, c, d μαζί. Το wrapper TPdfMatrix συνθέτει τις κοινές λειτουργίες για εσάς, έτσι ώστε οι Translate, Scale και Rotate να αλυσοδένονται με τη σειρά που τις καλείτε. Ένα διαγώνιο υδατογράφημα είναι μια περιστροφή ακολουθούμενη από μια μετατόπιση (translate) για επανακεντράρισμα. Ένα λογότυπο γωνίας είναι μια κλιμάκωση (scale) ακολουθούμενη από μια μετατόπιση. Όταν ο πίνακας είναι έτοιμος, παραδώστε την τιμή του στο FPDFPageObj_SetMatrix(PageObj, M.Handle), όπου M.Handle είναι το υποκείμενο FS_MATRIX. Το χαμηλότερου επιπέδου FPDFPageObj_Transform, το οποίο λαμβάνει τις έξι τιμές απευθείας ως doubles, είναι διαθέσιμο όταν προτιμάτε να περάσετε αριθμούς αντί να δημιουργήσετε ένα wrapper.
Σφράγιση κάθε σελίδας, με τη σωστή σειρά
Το πλήρες μοτίβο συνθέτει τα κομμάτια με τη σειρά που απαιτεί ο κανόνας διάρκειας ζωής. Ανοίξτε και τα δύο έγγραφα, καταγράψτε το stamp μία φορά, περιηγηθείτε στις σελίδες προορισμού επιλέγοντας καθεμία με τη σειρά και εισάγοντας συν τοποθετώντας ένα αντίγραφο, στη συνέχεια απελευθερώστε το XObject, μετά αποθηκεύστε και αφήστε το έγγραφο-πηγή να κλείσει τελευταίο.
procedure StampEveryPage(const ASource, AStamp, AOutput: string);
var
Dest, Stamp: TPdf;
XObject: TPdfXObject;
PageObj: FPDF_PAGEOBJECT;
M: TPdfMatrix;
i: Integer;
begin
Dest := TPdf.Create;
Stamp := TPdf.Create;
try
Dest.LoadFromFile(ASource);
Stamp.LoadFromFile(AStamp);
// 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.
for i := 0 to Dest.PageCount - 1 do
begin
Dest.CurrentPageIndex := 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
FPDFPageObj_SetMatrix(PageObj, M.Handle);
finally
M.Free;
end;
end;
finally
XObject.Free; // 3. free BEFORE Stamp closes
end;
// 4. Write the result while Dest is still open.
Dest.SaveLoadedDocument(AOutput);
finally
Stamp.Free; // source closes last
Dest.Free;
end;
end;
Το σχήμα των μπλοκ try κάνει την πραγματική δουλειά. Το εσωτερικό finally απελευθερώνει το XObject πριν ο έλεγχος φτάσει στο εξωτερικό finally που απελευθερώνει το Stamp, επομένως η λαβή απελευθερώνεται πάντα ενώ η πηγή της είναι ακόμα ζωντανή, ακόμη και αν προκύψει εξαίρεση κατά το βρόχο. Ρυθμίστε σωστά αυτό το φώλιασμα και ο κανόνας διάρκειας ζωής φροντίζει για τον εαυτό του.
Η σφράγιση είναι μια πτυχή μιας μεγαλύτερης εργαλειοθήκης για τη δημιουργία και την επεξεργασία περιεχομένου σελίδας. Εάν το stamp σας είναι εικόνα και όχι καταγεγραμμένη σελίδα, το άρθρο μετατροπή εικόνων σε έγγραφα PDF με το PDFium καλύπτει την εισαγωγή αυτού του bitmap σε ένα έγγραφο. Και όταν αυτό που θέλετε να μεταφέρετε είναι ένα αρχείο, η εργασία με συνημμένα PDF στο Delphi δείχνει την πλευρά των ενσωματωμένων αρχείων. Όλα αυτά αποστέλλονται με το PDFium Component για Delphi και C++Builder, μαζί με τα API απόδοσης, επεξεργασίας και εγγράφων που καλύπτονται αλλού σε αυτό το ιστολόγιο.