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

Ασφαλής Επανεγγραφή Ροών Περιεχομένου PDF στη Delphi

Το HotPDF διατρέχει κάθε τελεστή στη ροή περιεχομένου μιας σελίδας και τον παραδίδει στον κώδικά σας μέσω της VisitLoadedPageContentOperators. Για κάθε τελεστή επιλέγετε μια ενέργεια: διατήρηση, διαγραφή, αντικατάσταση με τα δικά σας bytes, διακοπή της διαδρομής, ή απόρριψη ολόκληρης της επεξεργασίας. Όταν κάθε τελεστής διατηρείται, η επανεγγεγραμμένη ροή είναι πανομοιότυπη κατά byte με το πρωτότυπο, κάτι που είναι η ιδιότητα που καθιστά τον μηχανισμό ασφαλή για εκτέλεση σε έγγραφα που δεν δημιουργήσατε εσείς

Η επεξεργασία ροών περιεχομένου είναι το σημείο όπου τα εργαλεία PDF συνήθως κάνουν λάθος. Μια ροή είναι μια ακολουθία τελεστών πάνω σε μια μηχανή στοίβας, όχι ένα δέντρο εγγράφου, και η αφελής προσέγγιση της εκτέλεσης μιας κανονικής έκφρασης πάνω στα αποκωδικοποιημένα bytes σπάει τη στιγμή που ένα κυριολεκτικό string περιέχει κάτι που μοιάζει με τελεστή, ή μια ενσωματωμένη εικόνα (inline image) περιέχει τυχαία δυαδικά δεδομένα. Ένας visitor που έχει ήδη διασπάσει τη ροή σε tokens δεν έχει αυτό το πρόβλημα

Τελεστές ροής περιεχομένου PDF όπως BT, Tf και TJ που ρέουν προς τα δεδομένα event του visitor με φάση, όνομα τελεστή, αριθμό τελεσταίων, byte span και κατάσταση φωλιάσματος, επιστρέφοντας κατόπιν μία από πέντε ενέργειες: διατήρηση, αντικατάσταση, διαγραφή, διακοπή ή απόρριψη
Κάθε τελεστής που επισκέπτεται αποδίδει ακριβώς μία απόφαση, και ένα πέρασμα που διατηρεί τα πάντα ξαναγράφει ανέγγιχτες περιοχές για έξοδο πανομοιότυπη κατά byte

Τι μπορείτε πραγματικά να κάνετε με πρόσβαση σε επίπεδο τελεστή;

Τρεις οικογένειες εργασιών. Έλεγχος (auditing): μέτρηση πόσες φορές μια σελίδα ορίζει ένα χρώμα εκτός γνωστού συνόλου, ή εύρεση κειμένου σχεδιασμένου με γραμματοσειρά που δεν είναι ενσωματωμένη. Αφαίρεση: διαγραφή ενός υδατογραφήματος που σχεδιάστηκε ως marked content, ή αφαίρεση μιας συγκεκριμένης κλήσης εικόνας. Και κανονικοποίηση: αντικατάσταση μιας μορφής τελεστή με μια ισοδύναμη που ένας μεταγενέστερος καταναλωτής χειρίζεται καλύτερα

Καθεμία από αυτές μοιράζεται μία απαίτηση: πρέπει να μπορείτε να αφήσετε το υπόλοιπο 99 τοις εκατό της ροής ανέγγιχτο. Το δέντρο σύνταξης του HotPDF αποθηκεύει ένα αντίγραφο της πηγής συν συμπαγή spans, και οι επεξεργασίες είναι ταξινομημένα, μη επικαλυπτόμενα overlays. Η εγγραφή του αποτελέσματος αντιγράφει απευθείας τις ανέγγιχτες περιοχές και εκπέμπει bytes αντικατάστασης μόνο εκεί όπου αλλάξατε κάτι, ώστε μια διαδρομή χωρίς καμία αλλαγή να μην μπορεί να εισαγάγει απόκλιση

Το συμβόλαιο του visitor

Υλοποιήστε τη διεπαφή ή αναθέστε το event, και κατόπιν καλέστε τη μέθοδο επίσκεψης με το ApplyChanges ορισμένο σε false για έλεγχο ή true για επανεγγραφή. Τα δεδομένα του event φέρουν τη φάση, το όνομα του τελεστή, τον αριθμό των τελεσταίων του, το byte span του, την τρέχουσα κατάσταση φωλιάσματος (nesting) και μια δανεισμένη προβολή για ανάγνωση τελεσταίων:

uses
  HPDFDoc, HPDFContentStream;

procedure TAuditor.HandleOperator(Sender: TObject;
  var EventData: THPDFContentOperatorEventData);
var
  Bytes: TBytes;
begin
  if EventData.Phase <> covpOperator then
    Exit;

  // Count text-showing operators drawn inside marked content
  if (EventData.OperatorName = 'Tj') or
     (EventData.OperatorName = 'TJ') then
  begin
    Inc(FTextOperators);
    if EventData.State.MarkedContentDepth > 0 then
      Inc(FTextInsideMarkedContent);
  end;

  // Read an operand only when you need it
  if EventData.OperatorName = 'Do' then
    if EventData.View.GetOperandBytes(0, Bytes) then
      NoteXObjectUse(TEncoding.ANSI.GetString(Bytes));

  EventData.Action := coaKeep;
end;

procedure TAuditor.Run(const FileName: string);
var
  Pdf: THotPDF;
  Stats: THPDFContentOperatorVisitStatistics;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile(FileName) <= 0 then
      Exit;
    Pdf.OnContentOperator := HandleOperator;
    for I := 0 to Pdf.LoadedPageCount - 1 do
      Pdf.VisitLoadedPageContentOperators(I, False, Stats);   // audit only
    Writeln(Format('%d operators visited, max q/Q depth %d',
      [Stats.VisitedOperatorCount, Stats.MaximumGraphicsStateDepth]));
  finally
    Pdf.Free;
  end;
end;

Γιατί η προβολή είναι δανεισμένη αντί για μεταβλητή;

Η callback δεν λαμβάνει ποτέ ένα τροποποιήσιμο δέντρο σύνταξης. Λαμβάνει μια προβολή που μπορεί να ανακτήσει τα bytes της τρέχουσας λειτουργίας ή έναν συγκεκριμένο τελεστέο κατ' απαίτηση, και αυτό είναι όλο. Κάθε αλλαγή περνά αντ' αυτού μέσα από τα πεδία ενέργειας και αντικατάστασης

Αυτή η έμμεση προσέγγιση δεν είναι τυπολατρία. Η άμεση τροποποίηση δέντρου θα επέτρεπε σε έναν visitor να παρακάμψει το όριο αντικατάστασης και τον έλεγχο επικαλυπτόμενων επεξεργασιών, και οι επικαλυπτόμενες επεξεργασίες σε ένα μοντέλο overlay byte-span παράγουν έξοδο που είναι σιωπηλά λανθασμένη αντί για ηχηρά σπασμένη. Η δρομολόγηση αλλαγών μέσω του πεδίου ενέργειας σημαίνει ότι κάθε επεξεργασία επικυρώνεται με τον ίδιο τρόπο ανεξάρτητα από τις προθέσεις του visitor

Η ανάγνωση κατ' απαίτηση διατηρεί επίσης φθηνή την κοινή περίπτωση. Ένας έλεγχος που απλώς μετρά ονόματα τελεστών δεν υλοποιεί ποτέ τα bytes τελεσταίων, κάτι που έχει σημασία σε μια ροή περιεχομένου με εκατοντάδες χιλιάδες λειτουργίες

Το πεδίο κατάστασης περιγράφει πάντα την είσοδο

Κάθε event φέρει το βάθος κατάστασης γραφικών, το βάθος αντικειμένου κειμένου, το βάθος marked content και το βάθος τμήματος συμβατότητας όπως ίσχυαν στην αρχική είσοδο αμέσως πριν εκτελεστεί αυτός ο τελεστής. Αυτό ισχύει ακόμη και αν ο visitor σας διέγραψε ή αντικατέστησε προηγούμενους τελεστές

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

Διαγραφή και αντικατάσταση, με τους κανόνες που σας προστατεύουν

Μια αντικατάσταση είναι ακατέργαστα bytes ροής περιεχομένου, και εσείς είστε υπεύθυνοι για τη σύνταξή τους. Οι διασφαλίσεις γύρω τους είναι το όριο στα συνολικά bytes αντικατάστασης, η απαίτηση μη επικάλυψης, και η προαιρετική επικύρωση εξόδου που επανασυντάσσει το αποτέλεσμα πριν γίνει αποδεκτό:

Διασφαλίσεις επανεγγραφής στη Delphi: ένας visitor που δηλώνει πρόθεση Action και Replacement δρομολογείται μέσω του ορίου αντικατάστασης, του μη επικαλυπτόμενου byte span και των ελέγχων επικύρωσης εξόδου προς την επικυρωμένη αποθηκευμένη σελίδα
Κάθε αλλαγή περνά μέσα από το πεδίο ενέργειας προς το όριο, την επικάλυψη και τους ελέγχους επανασύνταξης, ώστε μια αποτυχημένη επικύρωση να ματαιώνει την ενέργεια αντί να γράψει σπασμένους τελεστές
procedure TWatermarkStripper.HandleOperator(Sender: TObject;
  var EventData: THPDFContentOperatorEventData);
begin
  EventData.Action := coaKeep;
  if EventData.Phase <> covpOperator then
    Exit;

  // Drop the operators that paint the watermark form
  if (EventData.OperatorName = 'Do') and OperandIsWatermark(EventData) then
    EventData.Action := coaDelete
  // Normalise a stray colour to the house grey
  else if EventData.OperatorName = 'rg' then
  begin
    EventData.Replacement := TEncoding.ANSI.GetBytes('0.2 0.2 0.2 rg');
    EventData.Action := coaReplace;
  end
  // Refuse the entire edit if we meet something we do not understand
  else if EventData.OperatorName = 'BI' then
    EventData.Action := coaReject;
end;

var
  Options: THPDFContentOperatorVisitOptions;
begin
  Options := THPDFContentOperatorVisitOptions.Default;
  Options.ValidateOutput := True;        // reparse before accepting
  Options.MaxReplacementBytes := 64 * 1024;

  Pdf.OnContentOperator := HandleOperator;
  if Pdf.VisitLoadedPageContentOperators(PageIndex, True, Options, nil, Stats) then
    if Stats.Modified then
      Pdf.SaveLoadedDocument('cleaned.pdf');
end;

Το σύνολο ενεργειών περιλαμβάνει δύο εξόδους για έναν λόγο. Το coaStop τερματίζει τη διαδρομή και διατηρεί ό,τι έχει αποφασιστεί μέχρι τότε, κάτι που είναι σωστό όταν έχετε βρει αυτό που ψάχνατε. Το coaReject εγκαταλείπει ολόκληρη την επίσκεψη και αφήνει τη σελίδα ανέγγιχτη, κάτι που είναι σωστό όταν συναντάτε μια δομή που οι κανόνες επανεγγραφής σας δεν καλύπτουν. Οι ενσωματωμένες εικόνες (inline images) είναι το κλασικό παράδειγμα: το δυαδικό τους payload δεν είναι κάτι που ένας visitor κανονικοποίησης χρώματος θα έπρεπε να επεξεργάζεται γύρω του

Ανάγνωση των στατιστικών, και πού σταματά η διαδρομή

Το πεδίο κατάστασης διακρίνει την ολοκλήρωση από τη διακοπή, την απόρριψη, τη μη έγκυρη είσοδο, ένα υπερβολικό όριο, μη έγκυρη έξοδο και ένα σφάλμα visitor. Αυτά είναι διαφορετικά προβλήματα: μη έγκυρη έξοδος σημαίνει ότι τα bytes αντικατάστασής σας δεν επανασυντάχθηκαν, ενώ μη έγκυρη είσοδος σημαίνει ότι η πηγαία ροή ήταν ήδη κακοσχηματισμένη και δεν επιχειρήθηκε καμία επεξεργασία

Οι μετρητές καθιστούν έναν έλεγχο αυτο-επαληθεύσιμο. Το VisitedOperatorCount έναντι των KeptOperatorCount, DeletedOperatorCount και ReplacedOperatorCount πρέπει να αθροίζουν σωστά, και τα στοιχεία μέγιστου βάθους σας λένε αν η σελίδα φωλιάζει αρκετά βαθιά ώστε να αξίζει μια πιο προσεκτική εξέταση. Το SourceBytes έναντι του OutputBytes σας δίνει την επίδραση μεγέθους της επεξεργασίας σας σε έναν αριθμό

Συμφωνία στατιστικών ελέγχου όπου τα πλήθη διατηρημένων, διαγραμμένων και αντικατεστημένων τελεστών αθροίζουν στο πλήθος επισκεπτόμενων, δίπλα σε επτά καταλήξεις κατάστασης από ολοκληρωμένο έως σφάλμα visitor, πάνω από μια σημείωση ότι τα βάθη κατάστασης περιγράφουν την αρχική είσοδο ώστε οι έλεγχοι να επαναλαμβάνονται πανομοιότυπα
Μετρητές που πρέπει να αθροίζουν σωστά, μαζί με μια κατάσταση που ονομάζει κάθε πιθανή κατάληξη, καθιστούν έναν έλεγχο αυτο-επαληθεύσιμο σε έγγραφα που δεν δημιουργήσατε εσείς

Για τις συνήθεις εργασίες υπάρχουν διαδρομές υψηλότερου επιπέδου που δεν απαιτούν καθόλου τη συγγραφή ενός visitor. Η μόνιμη αφαίρεση περιεχομένου, με τον καθαρισμό πόρων που χρειάζεται μια πραγματική συγκάλυψη (redaction), καλύπτεται στο συγκάλυψη και συρραφή σελίδων σε φορτωμένα έγγραφα. Η αντικατάσταση ορατού κειμένου καλύπτεται στο αναζήτηση και αντικατάσταση κειμένου σε υπάρχον PDF. Χρησιμοποιήστε τον visitor τελεστών όταν ο κανόνας σας είναι δομικός και όχι κειμενικός, ή όταν χρειάζεστε έναν έλεγχο που καμία υπάρχουσα αναφορά δεν καλύπτει

Η επίσκεψη, η εξαγωγή και η επανεγγραφή ροών περιεχομένου εκτελούνται πάνω στο ίδιο μοντέλο φορτωμένου εγγράφου στη Delphi και τη C++Builder· η πλήρης λίστα χαρακτηριστικών βρίσκεται στη σελίδα του στοιχείου HotPDF Delphi PDF