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

Παρωχημένο κείμενο μετά την επεξεργασία: η προσωρινή μνήμη FPDF_TEXTPAGE του PDFium

Καλείτε το AddText για να σφραγίσετε μια γραμμή σε μια σελίδα PDF με το PDFiumPas, έπειτα καλείτε αμέσως το FindFirst για να επιβεβαιώσετε ότι προσγειώθηκε η σφραγίδα, και η αναζήτηση επιστρέφει άδεια. Το κείμενο είναι στη σελίδα — το Acrobat το δείχνει — αλλά το εξάρτημα TPdf του PDFiumPas κρατά μια ξεχωριστή αποθηκευμένη σε cache δομή FPDF_TEXTPAGE, αναλυμένη μία φορά από τη ροή περιεχομένου της σελίδας, και μια επεξεργασία δεν ενημερώνει αναδρομικά εκείνη τη δομή μόνη της. Κάντε ερώτημα σε αυτήν προτού ανανεωθεί και διαβάζετε τη σελίδα ακριβώς όπως φαινόταν πριν την αλλαγή σας, όχι μετά

Γιατί το PDFium επιστρέφει παρωχημένο κείμενο αμέσως μετά μια επεξεργασία;

Το PDFiumPas τυλίγει τη μηχανή απόδοσης PDFium της Google για Delphi και C++Builder, και οι κλήσεις κειμένου και επεξεργασίας του φτάνουν σε δύο διαφορετικά υποσυστήματα μέσα σε εκείνη τη μηχανή. Το FPDF_TEXTPAGE ανήκει στην πλευρά ανάγνωσης: το FPDFText_LoadPage διατρέχει τη ροή περιεχομένου της σελίδας μία φορά και χτίζει τη σελίδα κειμένου — κωδικοί χαρακτήρων, θέσεις, μετρήσεις γραμματοσειράς, όρια λέξεων — και το PDFiumPas κρατά εκείνη τη δομή αποθηκευμένη σε cache για όσο η σελίδα παραμένει φορτωμένη. Κλήσεις επεξεργασίας όπως το FPDFPage_InsertObject ή το FPDFPage_GenerateContent λειτουργούν σε μια εντελώς διαφορετική αναπαράσταση, το γράφημα αντικειμένων και ροής περιεχομένου της σελίδας, και το PDFium δεν προωθεί εκείνες τις αλλαγές σε μια ήδη ανοιχτή σελίδα κειμένου μόνο του. Η ανακατασκευή της σε κάθε επεξεργασία θα έκανε τη μαζική επεξεργασία απαράδεκτα αργή, οπότε ο σχεδιασμός ανταλλάσσει εκείνο το κόστος με έναν κανόνα αντ' αυτού — όποιος κρατά τον χειριστή τον κλείνει μετά από μια επεξεργασία που αλλάζει περιεχόμενο, και η επόμενη ανάγνωση χτίζει μια φρέσκια

Μέσα στην προσωρινή μνήμη κειμένου του TPdf: FTextPage, LoadTextPage, και UnloadTextPage

Το TPdf παρακολουθεί τον αποθηκευμένο σε cache χειριστή σε ένα μοναδικό ιδιωτικό πεδίο, FTextPage, και τυλίγει τον κύκλο ζωής του σε δύο μεθόδους. Το LoadTextPage ελέγχει αν το FTextPage είναι nil και, μόνο σε εκείνη την περίπτωση, καλεί το FPDFText_LoadPage έναντι της τρέχουσας σελίδας· αν ήδη υπάρχει χειριστής, το LoadTextPage τον επαναχρησιμοποιεί χωρίς να ρωτήσει αν η σελίδα άλλαξε από τότε που χτίστηκε. Το UnloadTextPage είναι το άλλο μισό: κλείνει τον γηγενή χειριστή με FPDFText_ClosePage, επαναφέρει το FTextPage σε nil, και επίσης αφαιρεί τη λίστα συνδέσμων ιστού σε cache και οποιαδήποτε συνεδρία εύρεσης σε εξέλιξη, αφού και οι δύο προήλθαν από την ίδια σελίδα κειμένου και παρωχούνται για τον ίδιο λόγο

Η συμπεριφορά επαναχρησιμοποίησης-χωρίς-έλεγχο του LoadTextPage είναι ακριβώς γιατί έχει σημασία η σειρά. Κάθε ερώτημα κειμένου στο TPdfText, FindFirst, GetWebLinks — περνά πρώτα μέσα από το LoadTextPage, οπότε όσο το FTextPage εξακολουθεί να κρατά τον προ-επεξεργασίας χειριστή, καμία από εκείνες τις κλήσεις δεν έχει κανέναν τρόπο να ξέρει ότι συνέβη μια αλλαγή. Η πλοήγηση σελίδας ποτέ δεν ήταν ο κίνδυνος εδώ: το UnloadPage, που τρέχει σε αλλαγές σελίδας, επαναφορτώσεις, και κλείσιμο εγγράφου, πάντα έκλεινε τη σελίδα κειμένου μαζί με την ίδια τη σελίδα. Το ανοιχτό ερώτημα ήταν πάντα σχετικά με επεξεργασίες που εφαρμόζονται στη σελίδα στην οποία εξακολουθείτε να κάθεστε

Ποιες μέθοδοι PDFiumPas ανανεώνουν αυτόματα την προσωρινή μνήμη;

Οι δικές του μέθοδοι επεξεργασίας σελίδας του TPdfAddText, SetText, SetTextPositions, AddPath, RemoveObject, και InsertFormObjectFromXObject — καθεμία καλεί το UnloadTextPage πριν καλέσει το UpdatePage (το FPDFPage_GenerateContent του PDFium) για να σειριοποιήσει την αλλαγή στη ροή περιεχομένου. Καλέστε οποιαδήποτε από αυτές και η αμέσως επόμενη κλήση Text, FindFirst, ή GetWebLinks ανακατασκευάζει τη σελίδα κειμένου από το περιεχόμενο όπως έχει τώρα, χωρίς καμία επιπλέον κλήση απαιτούμενη από την πλευρά σας

var
  Pdf: TPdf;
  Index: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'invoice.pdf';
    Pdf.Active := True;
    Pdf.PageNumber := 1;

    Pdf.AddText('Reviewed by J. Alvarez', 'Helvetica', 10, 72, 40, clBlack, 255, 0);
    // AddText already closed the cached text page, so this FindFirst
    // call rebuilds it fresh before it searches
    Index := Pdf.FindFirst('Reviewed by J. Alvarez');
    if Index >= 0 then
      ShowMessage('Stamp confirmed at character ' + IntToStr(Index));
  finally
    Pdf.Free;
  end;
end;

Το μοτίβο που εξακολουθεί να σπάει: η αποθήκευση σε cache του ακατέργαστου χειριστή TextPage

Το TPdf εκθέτει τον ζωντανό χειριστή μέσω μιας ιδιότητας TextPage μόνο για ανάγνωση, για τη σπάνια περίπτωση όπου χρειάζεστε να καλέσετε μια συνάρτηση FPDFText_* που το PDFiumPas δεν έχει τυλίξει. Εκείνη η διέξοδος διαφυγής είναι επίσης το μοναδικό σημείο όπου η αυτόματη ακύρωση δεν μπορεί να βοηθήσει: μόλις αντιγράψετε την τιμή FPDF_TEXTPAGE έξω από την ιδιότητα σε μια τοπική μεταβλητή, το PDFiumPas δεν έχει κανέναν τρόπο να ξέρει ότι εξακολουθείτε να την κρατάτε, και κανέναν τρόπο να ενημερώσει το αντίγραφό σας όταν το UnloadTextPage τρέξει κάπου αλλού στον κώδικά σας

var
  Pdf: TPdf;
  RawHandle: FPDF_TEXTPAGE;
  StaleCount: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'contract.pdf';
    Pdf.Active := True;
    Pdf.PageNumber := 1;

    RawHandle := Pdf.TextPage;    // FPDFText_LoadPage handle, cached in FTextPage
    Pdf.SetText(0, 'Amended Clause 4.2');
    // SetText already closed RawHandle and set Pdf.TextPage back to nil.
    // Calling any FPDFText_* function against the old value now touches a
    // handle PDFium has already freed — undefined behavior, not a bug you
    // can catch with a nil check
    StaleCount := FPDFText_CountChars(RawHandle);
  finally
    Pdf.Free;
  end;
end;

Η χρήση ενός χειριστή αφού το FPDFText_ClosePage έχει τρέξει πάνω του είναι απροσδιόριστη συμπεριφορά στο ίδιο το PDFium, όχι μια σύμβαση PDFiumPas που μπορείτε να επιλέξετε να αγνοήσετε — μπορεί να επιστρέψει τα τελευταία γνωστά δεδομένα, να μην επιστρέψει τίποτα, ή να καταρρεύσει τη διεργασία, και ποιο από αυτά συμβαίνει σε ένα δεδομένο build δεν είναι κάτι από το οποίο θα έπρεπε να εξαρτάται ο κώδικας εφαρμογής. Ο ασφαλής κανόνας είναι στενός: διαβάστε το Pdf.TextPage φρέσκο, αμέσως πριν την κλήση FPDFText_* που το χρειάζεται, και ποτέ μην κρατάτε ένα αντίγραφο κατά μήκος μιας δήλωσης που μπορεί να επεξεργαστεί τη σελίδα

Ομαδοποιήστε τις επεξεργασίες σας, έπειτα κάντε ερώτημα μία φορά

Τίποτα από αυτά δεν σημαίνει ότι κάθε κλήση AddText ή RemoveObject χρειάζεται ένα αμυντικό ερώτημα κειμένου αμέσως μετά για να ελέγξει το αποτέλεσμα. Κάθε μέθοδος επεξεργασίας ήδη πληρώνει το κόστος του κλεισίματος της σελίδας κειμένου μία φορά· το να κάνετε ερώτημα μετά από κάθε μεμονωμένη επεξεργασία μέσα σε έναν βρόχο πληρώνει εκείνο το κόστος ξανά χωρίς κανένα όφελος, αφού το FPDFText_LoadPage ξαναδιατρέχει ολόκληρη τη ροή περιεχομένου κάθε φορά που τρέχει

var
  Pdf: TPdf;
  I: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'watermarked.pdf';
    Pdf.Active := True;
    Pdf.PageNumber := 1;

    // Strip every text object that looks like a draft watermark. Each
    // RemoveObject call already invalidates the cache on its own, so
    // nothing needs refreshing by hand between iterations
    for I := Pdf.ObjectCount - 1 downto 0 do
      if (Pdf.ObjectType[I] = otText) and (Pdf.ObjectBounds[I].Top > 700) then
        Pdf.RemoveObject(I, True);

    // Query once, after the whole batch is done, not once per removal
    if Pdf.FindFirst('DRAFT') < 0 then
      ShowMessage('Watermark cleared');
  finally
    Pdf.Free;
  end;
end;

Η ίδια λογική ομαδοποίησης εφαρμόζεται ειδικά στην κατάσταση αναζήτησης. Τα FindNext και FindPrevious συνεχίζουν μια συνεδρία που ξεκίνησε το FindFirst, και εκείνη η συνεδρία γκρεμίζεται από το UnloadTextPage μαζί με όλα τα υπόλοιπα, οπότε η επανάκληση του FindNext μετά από μια επεξεργασία — αντί να καλέσετε ξανά το FindFirst — εγείρει εξαίρεση αντί να συνεχίσει σιωπηλά μια αναζήτηση έναντι περιεχομένου που δεν υπάρχει πλέον. Αντιμετωπίστε οποιαδήποτε επεξεργασία ως σκληρό όριο τόσο για το περιεχόμενο κειμένου όσο και για τη θέση αναζήτησης, και αφήστε ένα φρέσκο FindFirst στην άλλη πλευρά των επεξεργασιών σας να ξαναπιάσει την αναζήτηση

Πού ταιριάζει αυτό με την εργασία εξαγωγής και σχολιασμού

Η απλή εξαγωγή κειμένου — η ανάγνωση του κειμένου μιας σελίδας χωρίς να αλλάξει τίποτα — ποτέ δεν συναντά τίποτα από αυτά, επειδή τίποτα δεν ακυρώνει έναν χειριστή που καμία επεξεργασία δεν έχει αγγίξει. Για το πώς λειτουργούν το Text, τα ορθογώνια χαρακτήρων, και τα όρια λέξεων σε μια μη τροποποιημένη σελίδα, το συνοδευτικό άρθρο για την εξαγωγή κειμένου με το PDFiumPas καλύπτει εκείνο το έδαφος χωρίς τον κύκλο ζωής προσωρινής μνήμης σελίδας κειμένου που προσθέτει αυτό το άρθρο από πάνω

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

Τα API επεξεργασίας και κειμένου του TPdf είναι μέρος του εξαρτήματος PDFium για Delphi και C++Builder, και η σελίδα προϊόντος φέρει την πλήρη αναφορά μεθόδων για τις επιφάνειες επεξεργασίας, εξαγωγής, και αναζήτησης που καλύπτονται εδώ