Τεχνικό Άρθρο

Επαναχρησιμοποιήσιμες Σφραγίδες Σελίδας μέσω Form XObjects Με το PDFium

Η τοποθέτηση ενός υδατογραφήματος ή λογότυπου σε κάθε σελίδα ενός εγγράφου μοιάζει με δουλειά πέντε λεπτών, μέχρι να ανοίξετε το αποτέλεσμα σε έναν επιθεωρητή μεγέθους αρχείου. Η προφανής προσέγγιση είναι να διατρέξετε τις σελίδες και, σε κάθε μία, να δημιουργήσετε ξανά τα ίδια αντικείμενα κειμένου ή εικόνας. Αυτό λειτουργεί οπτικά, αλλά είναι σπάταλο με τρόπο που συσσωρεύεται. Ένα διαγώνιο υδατογράφημα "ΠΡΟΣΧΕΔΙΟ" που σχεδιάζεται απευθείας σε μια αναφορά εκατό σελίδων, αποτελεί εκατό αντίγραφα της ίδιας διαδρομής και δεδομένων κειμένου που βρίσκονται στις ροές περιεχομένου, και το αποθηκευμένο αρχείο τα περιέχει όλα

Ένα Form XObject είναι η δομή που παρέχει το PDF για να αποφύγει ακριβώς αυτό. Περικλείει ένα κομμάτι επαναχρησιμοποιήσιμου περιεχομένου, μια ολόκληρη σελίδα ή ένα μικρό πρότυπο, σε ένα μόνο ονομαστικό αντικείμενο που μπορεί να σχεδιαστεί πολλές φορές σε πολλές θέσεις. Το περιεχόμενο βρίσκεται στο αρχείο μία φορά. Κάθε σελίδα που θέλει τη σφραγίδα κρατά μια σύντομη εντολή που λέει "σχεδίασε το XObject N εδώ, με αυτόν τον μετασχηματισμό". Στη συνέχεια, ένα υδατογράφημα εκατό σελίδων προσθέτει ένα αντικείμενο περιεχομένου στο αρχείο αντί για εκατό, και αυτή είναι η διαφορά μεταξύ ενός εγγράφου που μεγαλώνει γραμμικά με τον αριθμό των σελίδων του και ενός που δεν το κάνει. Τα υδατογραφήματα, οι σφραγίδες λογότυπων, τα πρότυπα αριθμών σελίδων και οι σφραγίδες αποτελούν όλα την ίδια μορφή προβλήματος, και το Form XObject είναι το σωστό εργαλείο για καθένα από αυτά

Γιατί ένα αποθηκευμένο αντικείμενο είναι καλύτερο από εκατό επανασχεδιάσεις

Η εξοικονόμηση είναι δομική, όχι διακοσμητική. Μια σελίδα PDF αποδίδεται εκτελώντας τη ροή περιεχομένου της, μια ακολουθία τελεστών σχεδίασης. Όταν επανασχεδιάζετε μια σφραγίδα ανά σελίδα, προσαρτάτε την πλήρη ακολουθία τελεστών για αυτήν τη σφραγίδα στη ροή κάθε σελίδας, και τα byte διπλασιάζονται τόσες φορές όσες είναι και οι σελίδες σας. Ένα Form XObject μετακινεί αυτούς τους τελεστές σε μία ροή που αποθηκεύεται μία φορά στο έγγραφο. Η αναφορά που διατηρεί μια μεμονωμένη σελίδα είναι μικρή: προωθεί έναν πίνακα μετασχηματισμού, καλεί το XObject και επαναφέρει την κατάσταση. Ο αριθμός των σελίδων δεν πολλαπλασιάζει πλέον το κόστος των γραφικών

Αυτό έχει μεγαλύτερη σημασία όταν η σφραγίδα είναι βαριά. Μια διανυσματική σφραγίδα με εκατοντάδες τμήματα διαδρομής, ή ένα bitmap λογότυπου, είναι ακριβά στην αποθήκευση. Αποθηκευμένο μία φορά και με αναφορά σε αυτό, το βαρύ μέρος πληρώνεται μία φορά και η επιβάρυνση ανά σελίδα είναι μερικά byte επίκλησης. Το οπτικό αποτέλεσμα στη σελίδα είναι πανομοιότυπο με μια απευθείας επανασχεδίαση, το οποίο είναι και το ζητούμενο. Ο αναγνώστης δεν μπορεί να καταλάβει τη διαφορά· το μέγεθος του αρχείου όμως μπορεί, και με το παραπάνω

Καταγραφή μιας σελίδας σε ένα XObject

Το PDFium δημιουργεί το επαναχρησιμοποιήσιμο αντικείμενο από μια υπάρχουσα σελίδα. Η πηγή είναι μια σελίδα σε κάποιο έγγραφο που έχετε ανοιχτό, ένα μικρό μονοσέλιδο PDF που δεν περιέχει τίποτα άλλο εκτός από τα γραφικά του υδατογραφήματός σας, ή μια συγκεκριμένη σελίδα ενός μεγαλύτερου αρχείου. Το CreateXObjectFromPage καταγράφει το περιεχόμενο αυτής της σελίδας προέλευσης σε μια επαναχρησιμοποιήσιμη λαβή που ανήκει στο έγγραφο προορισμού, αυτό που σφραγίζετε

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

Η υπογραφή είναι CreateXObjectFromPage(Source: TPdf; SourcePageIndex: Integer): TPdfXObject. Η μέθοδος εγείρει εξαίρεση εάν το έγγραφο προέλευσης δεν είναι Active και επιστρέφει nil αντί να εγείρει εξαίρεση όταν το PDFium δεν μπορεί να δημιουργήσει το αντικείμενο, επομένως ο ρητός έλεγχος παραπάνω δεν είναι προαιρετικός. Η λαβή που επιστρέφεται είναι ένα TPdfXObject που σας ανήκει, και οι δύο περιορισμοί διάρκειας ζωής που συνδέονται με αυτό είναι το μέρος όλης αυτής της άσκησης που μπερδεύει τους χρήστες, γι' αυτό και έχουν τη δική τους ενότητα παρακάτω

Τοποθέτηση της σφραγίδας σε μια σελίδα

Ένα καταγεγραμμένο XObject δεν κάνει τίποτα από μόνο του. Για να εμφανιστεί, εισάγετε ένα αντίγραφό του στην τρέχουσα σελίδα του εγγράφου, αυτήν που επιλέγεται από την ιδιότητα PageNumber (με βάση το 1), με το InsertFormObjectFromXObject. Αυτή η κλήση επιστρέφει το υποκείμενο αντικείμενο σελίδας, ένα FPDF_PAGEOBJECT, και η λαβή που επιστρέφεται είναι ο τρόπος με τον οποίο τοποθετείτε τη σφραγίδα. Χωρίς μετασχηματισμό, η σφραγίδα προσγειώνεται στην αρχή των συντεταγμένων της σελίδας προέλευσης, κάτι που σπάνια είναι αυτό που θέλετε

Επειδή το InsertFormObjectFromXObject εισάγει ένα αντίγραφο ανά κλήση και επιστρέφει ένα νέο αντικείμενο σελίδας κάθε φορά, μπορείτε να σχεδιάσετε το ίδιο XObject πολλές φορές σε μία σελίδα με διαφορετικούς μετασχηματισμούς, και το αποθηκευμένο περιεχόμενο εξακολουθεί να μετράται μία φορά στο αρχείο. Ένα γωνιακό λογότυπο και ένα αχνό υδατογράφημα πλήρους σελίδας μπορούν να προέρχονται από το ίδιο καταγεγραμμένο αντικείμενο

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;

Δύο λεπτομέρειες διαχείρισης το καθιστούν ασφαλές. Πρώτον, μόλις εισαχθεί, το αντικείμενο σελίδας ανήκει στη σελίδα και όχι στο XObject. Η απελευθέρωση του XObject αργότερα δεν ακυρώνει τις τοποθετήσεις που ήδη κάνατε. Αυτό είναι που επιτρέπει στη σειρά 'δημιουργία-τοποθέτηση-απελευθέρωση' που περιγράφεται παρακάτω να λειτουργεί. Δεύτερον, η εισαγωγή και η τοποθέτηση αλλάζουν μόνο τη λίστα αντικειμένων της σελίδας στη μνήμη· η UpdatePage είναι αυτή που σειριοποιεί αυτή τη λίστα πίσω στη ροή περιεχομένου της σελίδας, επομένως μια σελίδα που επεξεργάζεστε χωρίς να την καλέσετε αποθηκεύεται σαν να μην είχε τοποθετηθεί ποτέ η σφραγίδα

Ο κανόνας διάρκειας ζωής της λαβής που μπερδεύει τους χρήστες

Δύο περιορισμοί διέπουν τη λαβή του XObject, και η αγνόηση οποιουδήποτε από τους δύο παράγει μια αποτυχία που φαίνεται άσχετη με την αιτία της. Πρώτον, το έγγραφο προέλευσης πρέπει να είναι ενεργό τη στιγμή που καλείτε το CreateXObjectFromPage. Η καταγραφή διαβάζει το περιεχόμενο της σελίδας προέλευσης από το ζωντανό έγγραφο προέλευσης, επομένως αυτό το έγγραφο και η σελίδα του πρέπει να είναι ανοιχτά και έγκυρα όταν δημιουργείται η λαβή. Δεύτερον, και αυτό είναι που εκπλήσσει τους χρήστες, η λαβή πρέπει να απελευθερωθεί πριν κλείσει η σελίδα προέλευσης, και στην πράξη πριν κλείσετε ή απελευθερώσετε το έγγραφο προέλευσης από το οποίο προήλθε

Ο λόγος είναι ότι το XObject είναι μια αναφορά σε δομή που εξακολουθεί να κατέχει το έγγραφο προέλευσης. Δεν είναι ένα αποσπασμένο, αυτόνομο αντίγραφο που μπορείτε να μεταφέρετε αφού η πηγή έχει χαθεί. Αν κλείσετε πρώτα την πηγή, η λαβή μένει να δείχνει σε περιεχόμενο που έχει καταστραφεί, οπότε η απελευθέρωσή της αργότερα, ή οποιαδήποτε άλλη χρήση της, λειτουργεί σε μνήμη που δεν είναι πλέον έγκυρη. Το σύμπτωμα είναι το κλασικό για μια λαβή που εκκρεμεί: μια παραβίαση πρόσβασης (access violation) κατά τον τερματισμό, ή διαλείπουσα καταστροφή δεδομένων που μετακινείται ανάλογα με τη σειρά κατανομής, με μια στοίβα που δείχνει στον κώδικα εκκαθάρισης μάλλον παρά στη γραμμή που προκάλεσε πραγματικά το πρόβλημα. Η λύση είναι η σειρά εκτέλεσης, όχι ο αμυντικός προγραμματισμός. Δημιουργήστε το XObject, εισαγάγετέ το σε κάθε σελίδα που το χρειάζεται, απελευθερώστε το XObject και μόνο τότε κλείστε το έγγραφο προέλευσης. Ο καταστροφέας (destructor) του TPdfXObject απελευθερώνει την υποκείμενη λαβή του PDFium για εσάς, επομένως η απελευθέρωση του wrapper τη σωστή στιγμή είναι αποκλειστικά δική σας ευθύνη

Ο πίνακας μετασχηματισμού και τι σημαίνουν οι έξι αριθμοί του

Η τοποθέτηση είναι ένας δισδιάστατος (2D) αφινικός μετασχηματισμός, ο ίδιος που χρησιμοποιεί το 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)

Μπορείτε να συμπληρώσετε αυτές τις έξι τιμές με το χέρι, αλλά η σύνθεσή τους με το χέρι είναι το σημείο όπου η περιστροφή πάει στραβά, επειδή η περιστροφή αναμιγνύει και τα τέσσερα a, b, c, d μαζί. Ο wrapper TPdfMatrix, από τη μονάδα FPdfMatrix, συνθέτει τις κοινές λειτουργίες για εσάς και πολλαπλασιάζει εκ των υστέρων καθώς προχωρά, έτσι ώστε τα Translate, Scale και Rotate να αλυσιδώνονται με τη σειρά που τα καλείτε. Ένα διαγώνιο υδατογράφημα είναι μια περιστροφή που ακολουθείται από μια μετάφραση για να επανακεντραριστεί· ένα γωνιακό λογότυπο είναι μια κλιμάκωση που ακολουθείται από μια μετάφραση. Όταν ο πίνακας είναι έτοιμος, αντιγράψτε την ακατέργαστη τιμή του, την ιδιότητα Handle τύπου FS_MATRIX, σε μια τοπική μεταβλητή και περάστε την στο FPDFPageObj_SetMatrix· η εισαγωγή δηλώνει τον πίνακα ως παράμετρο var, επομένως μια ιδιότητα δεν μπορεί να παραδοθεί απευθείας σε αυτό, και το αποτέλεσμά της είναι 0 σε περίπτωση αποτυχίας. Το FPDFPageObj_Transform χαμηλότερου επιπέδου, το οποίο δέχεται τις έξι τιμές απευθείας ως doubles, είναι διαθέσιμο όταν προτιμάτε να περάσετε αριθμούς αντί να δημιουργήσετε έναν wrapper

Σφράγιση κάθε σελίδας, με τη σωστή σειρά

Το πλήρες μοτίβο συνδυάζει τα κομμάτια με τη σειρά που απαιτεί ο κανόνας διάρκειας ζωής. Ανοίξτε και τα δύο έγγραφα, καταγράψτε τη σφραγίδα μία φορά, διατρέξτε τις σελίδες προορισμού ρυθμίζοντας διαδοχικά το PageNumber (με βάση το 1) και εισάγοντας συν τοποθετώντας ένα αντίγραφο, δεσμεύοντας κάθε σελίδα με το UpdatePage, στη συνέχεια απελευθερώστε το XObject, μετά αποθηκεύστε με το SaveAs, και αφήστε το έγγραφο προέλευσης να κλείσει τελευταίο

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;

Το σχήμα των μπλοκ try κάνει την πραγματική δουλειά. Το εσωτερικό finally απελευθερώνει το XObject πριν ο έλεγχος μπορέσει ποτέ να φτάσει στο εξωτερικό finally που απελευθερώνει το Stamp, έτσι ώστε η λαβή να απελευθερώνεται πάντα ενώ η πηγή της είναι ακόμα ζωντανή, ακόμη και αν προκύψει μια εξαίρεση στη μέση του βρόχου. Κάντε σωστά αυτή την ένθεση και ο κανόνας διάρκειας ζωής φροντίζει τον εαυτό του

Η σφράγιση είναι μια γωνία μιας μεγαλύτερης εργαλειοθήκης για τη δημιουργία και επεξεργασία περιεχομένου σελίδας. Αν η σφραγίδα σας είναι η ίδια μια εικόνα παρά μια καταγεγραμμένη σελίδα, η μετατροπή εικόνων σε έγγραφα PDF με το PDFium καλύπτει πρώτα την εισαγωγή αυτού του bitmap σε ένα έγγραφο. Και όταν αυτό που θέλετε να μεταφέρετε παράλληλα με την ορατή σφραγίδα είναι ένα αρχείο παρά μελάνι στη σελίδα, η εργασία με συνημμένα αρχεία PDF στο Delphi δείχνει την πλευρά του ενσωματωμένου αρχείου. Όλα αυτά διατίθενται με το PDFium Component για Delphi και C++Builder, παράλληλα με τα API απόδοσης, επεξεργασίας και εγγράφων που καλύπτονται αλλού σε αυτό το ιστολόγιο