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

Αναζήτηση και αντικατάσταση κειμένου σε υπάρχον PDF στο Delphi

Το HotPDF Component μπορεί να πραγματοποιήσει αναζήτηση και αντικατάσταση κειμένου μέσα σε ένα υπάρχον PDF από το Delphi και το C++Builder. Οι SearchLoadedPageText και SearchLoadedDocumentText εντοπίζουν κάθε εμφάνιση μιας συμβολοσειράς με ακρίβεια σε επίπεδο γλύφων, και οι ReplaceLoadedPageText και ReplaceLoadedDocumentText ξαναγράφουν τα αντίστοιχα bytes επί τόπου — με την προϋπόθεση ότι κάθε χαρακτήρας αντικατάστασης μπορεί να επανακωδικοποιηθεί μέσω της αρχικής γραμματοσειράς, ένας φυσικός περιορισμός τον οποίο αυτό το άρθρο αντιμετωπίζει με ειλκρίνεια αντί να τον κρύβει σε μια υποσημείωση

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

Γιατί είναι τόσο δύσκολη η αντικατάσταση κειμένου σε ένα PDF;

Η αντικατάσταση κειμένου σε ένα PDF είναι δύσκολη επειδή μια σελίδα PDF δεν περιέχει επεξεργάσιμο κείμενο — περιέχει τοποθετημένες γλύφες. Στο πλαίσιο του μοντέλου εμφάνισης κειμένου του ISO 32000-1 §9.4, μια ροή περιεχομένου (content stream) κατευθύνει τελεστές όπως οι Tj και TJ που σχεδιάζουν ακολουθίες κωδικών χαρακτήρων σε συντεταγμένες που καθορίζονται από τον πίνακα κειμένου. Αυτοί οι κωδικοί δεν είναι Unicode· είναι δείκτες σε οποιαδήποτε κωδικοποίηση δηλώνει η γραμματοσειρά της σελίδας, και η αντιστοίχιση πίσω σε αναγνώσιμους χαρακτήρες μπορεί να βρίσκεται σε έναν πίνακα /ToUnicode CMap, σε έναν πίνακα διαφορών κωδικοποίησης (encoding difference array) ή σε μια αλυσίδα αντιστοίχισης CID. Δεν υπάρχει αντικείμενο παραγράφου, ροή κειμένου και καμία εγγύηση ότι μια οπτική λέξη αποθηκεύεται καν ως μία συμβολοσειρά

Η αντικατάσταση προσθέτει ένα δεύτερο επίπεδο δυσκολίας πάνω από την αποκωδικοποίηση: πρέπει να γνωρίζετε ακριβώς ποια bytes της αρχικής ροής παρήγαγαν κάθε γλύφο, ώστε να μπορείτε να εισάγετε νέα bytes ακριβώς σε αυτό το διάστημα και πουθενά αλλού. Ένας εξαγωγέας κειμένου μπορεί να αντέξει να απορρίψει τις θέσεις των bytes μόλις εξαγάγει το Unicode. Ένας αντικαταστάτης δεν μπορεί. Γι' αυτό το HotPDF χώρισε τη δουλειά σε δύο εκδόσεις — η v2.251.0 δημιούργησε το επίπεδο εντοπισμού μετατόπισης και αναζήτησης, και η v2.252.0 έχτισε το επίπεδο επανεγγραφής πάνω σε αυτό

Εύρεση κειμένου: αναζήτηση σε επίπεδο γλύφων με εντοπισμό μετατόπισης byte

Η SearchLoadedDocumentText του HotPDF βρίσκει κάθε εμφάνιση μιας συμβολοσειράς κάνοντας αντιστοίχιση με την αποκωδικοποιημένη ακολουθία γλυφών Unicode κάθε σελίδας, notch με τα ακατέργαστα bytes της ροής, οπότε μια επιτυχία είναι επιτυχία ανεξάρτητα από τον τρόπο με τον οποίο την κωδικοποίησε η γραμματοσειρά. Η υποκείμενη υποδομή εισήχθη στην έκδοση v2.251.0: ο tokenizer της ροής περιεχομένου καταγράφει ένα εύρος byte StartOfs/EndOfs για κάθε τελεστέο συμβολοσειράς — συμπεριλαμβανομένων των οριοθετών του ( ) ή < > — και κάθε αποκωδικοποιημένη γλύφος φέρει μια τριάδα TokenIndex/ItemIndex/ByteOffset που δείχνει πίσω στον ακριβή τελεστέο, στο στοιχείο πίνακα TJ και στη μονάδα κώδικα που την παρήγαγε. Ο ίδιος διερμηνέας γλυφών τροφοδοτεί το API εξαγωγής που περιγράφεται στο άρθρο εξαγωγή κειμένου από φορτωμένο PDF στο Delphi· η αναζήτηση απλώς διατηρεί την προέλευση που η εξαγωγή απορρίπτει

Κάθε αντιστοίχιση επιστρέφει ως εγγραφή THPDFTextMatch που μεταφέρει τον δείκτη σελίδας, το εύρος των γλυφών, την προέλευση X/Y και το πλάτος της επιτυχίας στο χώρο του χρήστη, το διακριτικό προέλευσης (source token) και τον δείκτη στοιχείου, καθώς και το ίδιο το κείμενο που αντιστοιχίστηκε. Αυτό είναι αρκετό για να οδηγήσει σε μια επικάλυψη επισήμανσης, μια διεπαφή ελέγχου ή το βήμα αντικατάστασης. Μια αναζήτηση που δεν βρίσκει τίποτα επιστρέφει έναν άδειο πίνακα αντί να αποτύχει, οπότε το μοτίβο κλήσης παραμένει απλό

var
  Pdf: THotPDF;
  Matches: THPDFTextMatchArray;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('invoices-2025.pdf') > 0 then
    begin
      if Pdf.SearchLoadedDocumentText('Acme Corp', False, Matches) then
        for I := 0 to Length(Matches) - 1 do
          WriteLn(Format('page %d at (%.1f, %.1f): "%s"',
            [Matches[I].PageIndex, Matches[I].X, Matches[I].Y,
             Matches[I].Text]));
    end;
  finally
    Pdf.Free;
  end;
end;

Μια σκόπιμη σχεδιαστική επιλογή αξίζει να σημειωθεί. Όναν το CaseSensitive είναι False, η σύγκριση αναδιπλώνει την κατάσταση πεζών-κεφαλαίων (case folding) μόνο για χαρακτήρες ASCII, από σχεδιασμό: η πλήρης αναδίπλωση πεζών-κεφαλαίων Unicode συμπεριφέρεται διαφορετικά στις εργαλειοσειρές Delphi 5 έως XE που υποστηρίζει το HotPDF, και ένα API αναζήτησης που βρίσκει διαφορετικές αντιστοιχίσεις ανάλογα με το ποιος μεταγλωττιστής δημιούργησε την εφαρμογή σας είναι χειρότερο από ένα με τεκμηριωμένο, προβλέψιμο όριο. Για λατινικό επιχειρηματικό κείμενο — ονόματα, κωδικούς, ημερομηνίες — η αναδίπλωση ASCII καλύπτει τις πρακτικές περιπτώσεις

Αντικατάσταση κειμένου: αντίστροφη κωδικοποίηση και χειρουργική εισαγωγή

Η ReplaceLoadedDocumentText, η οποία προστέθηκε στην έκδοση HotPDF v2.252.0, ξαναγράφει κάθε εμφάνιση μιας συμβολοσειράς αναζήτησης εκτελώντας τον μηχανισμό αποκωδικοποίησης προς τα πίσω. Η συνάρτηση HPDFEncodeUnicode είναι το αντίστροφο του αποκωδικοποιητή κωδικών χαρακτήρων: διατρέχει την ίδια αλυσίδα στρατηγικής αντίστροφα — αναζήτηση bfchar και bfrange στο /ToUnicode, αντιστοίχιση CID ροής κωδικοποίησης, αντιστοιχίσεις ταυτότητας Type0 και τους προκαθορισμένους πίνακες WinAnsi και MacRoman — για να μετατρέψει κάθε χαρακτήρα αντικατάστασης πίσω στα bytes κωδικού χαρακτήρα που αναμένει η αρχική γραμματοσειρά. Τα επανακωδικοποιημένα bytes σειριοποιούνται στη συνέχεια σε ένα καλοσχηματισμένο λεκτικό συμβολοσειράς ή δεκαεξαδική συμβολοσειρά, αντικατοπτρίζοντας τους κανόνες διαφυγής του ίδιου του tokenizer, έτσι ώστε ο κύκλος ανάλυσης → επανασειριοποίησης να είναι σταθερός

Η ίδια η εισαγωγή είναι χειρουργική και όχι μαζική. Μόνο το εύρος των bytes κώδικα που καλύπτεται από την αντιστοίχιση αντικαθίσταται μέσα στον τελεστέο συμβολοσειράς· τα μη αντιστοιχισμένα bytes στον ίδιο τελεστέο, το κενό διάστημα μεταξύ των διακριτικών (tokens) και κάθε γύρω τελεστής διατηρούνται αυτολεξεί, byte προς byte. Η αντικατάσταση του bca μέσα στο abcabc παράγει a + αντικατάσταση + bc, όχι έναν κατεστραμμένο τελεστέο. Οι αντικαταστάσεις μπορεί να είναι μικρότερες ή μεγαλύτερες από τη συμβολοσειρά αναζήτησης — το λεκτικό επανασειριοποιείται και το /Length της ροής ανανεώνεται — και κάθε ροή /Contents μιας σελίδας πολλαπλών ροών επεξεργάζεται μεμονωμένα, ώστε η σελίδα να παραμένει καλοσχηματισμένη

var
  Pdf: THotPDF;
  ReplaceCount: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('contract-draft.pdf') > 0 then
    begin
      if Pdf.ReplaceLoadedDocumentText('2025-12-31', '2026-12-31',
        True, ReplaceCount) then
        WriteLn(Format('%d operand rewrites performed', [ReplaceCount]));
      Pdf.SaveLoadedDocument('contract-final.pdf');
    end;
  finally
    Pdf.Free;
  end;
end;

Σημειώστε τι δεν κάνει το API: δεν αναστοιχειοθετεί τη σελίδα. Το PDF δεν έχει αυτόματη ροή (reflow), οπότε μια αντικατάσταση που είναι οπτικά πλατύτερη από την αρχική θα καταλάβει απλώς περισσότερο οριζόντιο χώρο και μπορεί να συνωστιστεί με ό,τι έχει σχεδιαστεί στα δεξιά της. Οι αντικαταστάσεις ίδιου ή παρόμοιου μήκους — ημερομηνίες, συμβολοσειρές εκδόσεων, αριθμοί εξαρτημάτων, διορθώσεις ονομάτων — είναι το ιδανικό σημείο χρήσης. Η μαζική αναδιατύπωση ανήκει στο έγγραφο προέλευσης, όχι στο PDF

Γιατί δεν μπορείτε να αντικαταστήσετε κείμενο με χαρακτήρες που το υποσύνολο της γραμματοσειράς δεν περιέλαβε ποτέ;

Δεν μπορείτε να αντικαταστήσετε κείμενο με έναν χαρακτήρα που το ενσωματωμένο υποσύνολο της γραμματοσειράς δεν περιέλαβε ποτέ, επειδή η ακολουθία bytes που θα επέλεγε αυτόν τον χαρακτήρα απλά δεν υπάρχει στους πίνακες αντιστοίχισης της γραμματοσειράς. Όταν ένας παραγωγός PDF ενσωματώνει μια γραμματοσειρά υποσυνόλου, ο πίνακας /ToUnicode CMap και οι δομές κωδικοποίησης καλύπτουν μόνο τις γλύφες που χρησιμοποίησε πραγματικά το αρχικό έγγραφο. Η HPDFEncodeUnicode μπορεί να αντιστρέψει μόνο μια αντιστοίχιση που είναι παρούσα: εάν το έγγραφο δεν περιείχε ποτέ το γράμμα E στη συγκεκριμένη γραμματοσειρά, δεν υπάρχει κωδικός χαρακτήρα για το E για να γίνει η αντιστροφή. Αυτό είναι μια φυσική ιδιότητα του αρχείου, όχι περιορισμός κάποιας συγκεκριμένης βιβλιοθήκης — κανένα εργαλείο δεν μπορεί να επινοήσει μια αντιστοίχιση γλύφου που δεν ενσωματώθηκε ποτέ

Το HotPDF χειρίζεται την αποτυχία συντηρητικά. Εάν οποιοσδήποτε μεμονωμένος χαρακτήρας της αντικατάστασης δεν μπορεί να επανακωδικοποιηθεί, ολόκληρη αυτή η εμφάνιση της συμβολοσειράς αναζήτησης παρακάμπτεται — χωρίς εξαίρεση, χωρίς μερικό κατεστραμμένο κείμενο, και η εμφάνιση απλώς δεν προσμετράται στο ReplaceCount. Η πρακτική συνέπεια: ελέγξτε το ReplaceCount έναντι του αριθμού αντιστοιχίσεων από μια προηγούμενη αναζήτηση, και αντιμετωπίστε μια υστέρηση ως σήμα. Στο παράδειγμα της ημερομηνίας παραπάνω, το ψηφίο 6 πρέπει να εμφανίζεται κάπου στο κείμενο του εγγράφου στην ίδια γραμματοσειρά για να πετύχει η επανεγγραφή — πιθανό σε ένα τιμολόγιο, ποτέ εγγυημένο γενικά. Όταν οι χαρακτήρες που χρειάζεστε απλά δεν είναι διαθέσιμοι και ο στόχος είναι η αφαίρεση ευαίσθητου κειμένου αντί για την αναδιατύπωσή του, η πραγματική αφαίρεση περιεχομένου είναι ούτως ή άλλως το καλύτερο εργαλείο· δείτε το άρθρο απόκρυψη και αναδιάρθρωση φορτωμένων PDF στο Delphi για αυτή τη διαδρομή

var
  Matches: THPDFTextMatchArray;
  Expected, Replaced: Integer;
begin
  Pdf.SearchLoadedDocumentText('Acme Corp', True, Matches);
  Expected := Length(Matches);
  Pdf.ReplaceLoadedDocumentText('Acme Corp', 'Apex Corp', True, Replaced);
  if Replaced < Expected then
    WriteLn(Format('%d occurrence(s) skipped: characters missing ' +
      'from the font subset, or match spans multiple operands',
      [Expected - Replaced]));
end;

Η δεύτερη συνθήκη παράκαμψης σε αυτό το μήνυμα είναι το άλλο τεκμηριωμένο όριο: μια συμβολοσειρά αναζήτησης που εκτείνεται σε πολλούς τελεστέους συμβολοσειράς — το Hello χωρισμένο σε στοιχεία [(He)(llo)] TJ, για παράδειγμα — εντοπίζεται από την αναζήτηση, επειδή η αναζήτηση ταιριάζει με την αποκωδικοποιημένη ακολουθία γλυφών, αλλά παρακάμπτεται από την αντικατάσταση, επειδή η επανεγγραφή μεταξύ ορίων τελεστέων θα απαιτούσε τη συγχώνευση γειτονικών διαστημάτων byte. Η αναζήτηση και στη συνέχεια η επαλήθευση καθιστά και τα δύο όρια ορατά αντί για σιωπηρά

Τι αλλάζει στο αρχείο κατά την αποθήκευση;

Μια αντικατασταθείσα ροή /Contents αποθηκεύεται ασυμπίεστη. Οι συμπιεσμένες με FlateDecode ροές αποσυμπιέζονται για επεξεργασία, και όταν το HotPDF γράφει τα ανακατασκευασμένα bytes, αφαιρεί την καταχώριση /Filter της ροής και ανανεώνει το /Length αντί να κάνει επανασυμπίεση. Το προκύπτον PDF είναι πλήρως έγκυρο και αποδίδεται κανονικά στις κύριες εφαρμογές προβολής· η ανταλλαγή είναι ένα μεγαλύτερο αρχείο για κάθε επεξεργασμένη ροή. Για μια μαζική ροή εργασίας που επεξεργάζεται χιλιάδες έγγραφα, προϋπολογίστε αυτήν την αύξηση ή εκτελέστε ένα ξεχωριστό πέρασμα συμπίεσης στη συνέχεια. Το πώς αλληλεπιδρούν τα ξαναγραμμένα αντικείμενα με τη δομή παραπομπών του εγγράφου κατά την αποθήκευση είναι το δικό του θέμα, που καλύπτεται στο άρθρο ροές αντικειμένων και σταδιακές ενημερώσεις στο HotPDF

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

Η αναζήτηση και αντικατάσταση κειμένου ενώνεται με την εξαγωγή, την απόκρυψη και την απόδοση σελίδας στο σύνολο εργαλείων φορτωμένων εγγράφων του HotPDF, όλα καθοδηγούμενα από τον ίδιο διερμηνέα ροής περιεχομένου και διαθέσιμα από το Delphi 5 έως τις τρέχουσες εκδόσεις του RAD Studio χωρίς εξωτερικές εξαρτήσεις. Η πλήρης αναφορά API και η δοκιμαστική λήψη βρίσκονται στη σελίδα προϊόντος του HotPDF Component