Η συγχώνευση ή η διαίρεση ενός PDF δύο gigabyte με τον προφανή τρόπο σας κοστίζει δύο πράγματα ταυτόχρονα: πραγματικό χρόνο και χώρο διευθύνσεων. Ο προφανής τρόπος είναι να φορτώσετε κάθε είσοδο, να κάνετε τη δουλειά, να γράψετε την έξοδο. Η φόρτωση είναι το σημείο όπου σπάει. Ένα αρχείο σαρώσεων που περνά από τα 300 στα 600 DPI διπλασιάζει τη γραμμική του ανάλυση και περίπου τετραπλασιάζεται στον δίσκο, οπότε η ίδια εργασία συναρμολόγησης που χειριζόταν αρχεία 400 MB όλη τη χρονιά αρχίζει να καταρρέει τη στιγμή που μια είσοδος ξεπερνά το gigabyte, συχνά ενώ δεν κάνει τίποτα περισσότερο από το να μετράει σελίδες. Η εργασία δεν έγινε ποτέ δυσκολότερη. Άνοιγμα, μέτρημα, επιλογή περιοχών, συνένωση — αυτό είναι όλο. Η πλήρης φόρτωση του δέντρου απλώς έπαψε να είναι λογική προεπιλογή σε αυτό το μέγεθος. Το PDF Library for Delphi, η βιβλιοθήκη PDF της losLab για Delphi και C++Builder, απαντά σε αυτό με το επίπεδο Direct Access: μια οικογένεια συναρτήσεων με πρόθεμα DA που στηρίζεται σε έναν streaming reader ο οποίος διατρέχει επιτόπου τον πίνακα διασταυρούμενων αναφορών αντί να χτίζει ολόκληρο το έγγραφο στη μνήμη
Πού πάει η μνήμη σε μια πλήρη φόρτωση
Η «κανονική» φόρτωση ενός PDF σημαίνει ανάλυση του xref, επίλυση κάθε έμμεσου αντικειμένου σε ένα δέντρο μέσα στη μνήμη, αποκωδικοποίηση των object streams και σύνδεση του δέντρου σελίδων, των γραμματοσειρών και των σχολιασμών σε αντικείμενα που μπορείτε να χειριστείτε. Για ροές εργασίας επεξεργασίας αυτή είναι η σωστή ανταλλαγή. Για δουλειά συγχώνευσης, διαίρεσης και επιθεώρησης είναι ως επί το πλείστον σπατάλη. Ένα αρχείο σαρώσεων 30.000 σελίδων μπορεί να περιέχει εκατομμύρια έμμεσα αντικείμενα, ενώ μια εργασία διαίρεσης χρειάζεται να διαβάσει μερικές εκατοντάδες από αυτά: τους κόμβους σελίδων στη ζητούμενη περιοχή, συν ό,τι αναφέρουν αυτοί οι κόμβοι
Το επίπεδο Direct Access αντιστρέφει το μοντέλο. Οι DAOpenFile και DAOpenFileReadOnly αναλύουν το trailer και το xref, μερικά kilobyte στην ουρά του αρχείου, και επιστρέφουν ένα handle αρχείου. Τα αντικείμενα ανακτώνται τεμπέλικα όταν τα χρειάζεται κάποια κλήση. Η πρακτική συνέπεια είναι ότι το άνοιγμα ενός αρχείου πολλών gigabyte διαρκεί περίπου όσο και το άνοιγμα ενός μικρού, και η μνήμη ακολουθεί αυτό που αγγίζετε αντί για αυτό που περιέχει το αρχείο
Εξέταση ενός τεράστιου αρχείου χωρίς να το φορτώσετε
Το μοτίβο που ακολουθεί προέρχεται από το ίδιο το benchmark μεγάλων αρχείων της βιβλιοθήκης: άνοιγμα μόνο για ανάγνωση, ερωτήματα, κλείσιμο. Κανένα δέντρο εγγράφου δεν υπάρχει ποτέ
var
Lib: TPDFlib;
Handle, Pages: Integer;
begin
Lib := TPDFlib.Create;
try
Handle := Lib.DAOpenFileReadOnly('archive-2025.pdf', '');
if Handle = 0 then
raise Exception.Create('Direct access open failed');
Pages := Lib.DAGetPageCount(Handle);
Writeln('pages : ', Pages);
Writeln('title : ', Lib.DAGetInformation(Handle, 'Title'));
Lib.DACloseFile(Handle);
finally
Lib.Free;
end;
end;
Η λειτουργία μόνο για ανάγνωση αξίζει να προτιμάται όποτε γίνεται: επιτρέπει στο στάδιο παραλαβής να τρέχει ενώ άλλες διεργασίες κρατούν το αρχείο, και τεκμηριώνει την πρόθεση. Ένα στάδιο εξέτασης που καλεί κατά λάθος μια συνάρτηση μεταβολής αποτυγχάνει γρήγορα αντί να καταστρέψει το αρχείο
Το PageRef είναι handle αντικειμένου, όχι αριθμός σελίδας
Το μακράν πιο συνηθισμένο λάθος με το DA API είναι το πέρασμα ενός αριθμού σελίδας εκεί όπου μια συνάρτηση περιμένει ένα PageRef. Σχεδόν κάθε κλήση DA ανά σελίδα δέχεται ένα handle αναφοράς προς το αντικείμενο της σελίδας και όχι έναν αριθμό σελίδας: οι DAExtractPageText, DARenderPageToFile, DARotatePage και DACapturePage περιμένουν όλες ref. Το αποκτάτε μεταφράζοντας τον αριθμό που βλέπει ο άνθρωπος μέσω της DAFindPage:
PageRef := Lib.DAFindPage(Handle, 250); // αριθμός σελίδας -> handle αντικειμένου
if PageRef <> 0 then
begin
Text := Lib.DAExtractPageText(Handle, PageRef, 0);
Lib.DARenderPageToFile(Handle, PageRef, 5, 150, 'page250.png');
end;
Το πέρασμα του σκέτου αριθμού 250 δεν προκαλεί σφάλμα. Απευθύνεται σε όποιο αντικείμενο τυχαίνει να βρίσκεται πίσω από αυτή την τιμή handle, πράγμα που μια καλή μέρα αποτυγχάνει ορατά και μια κακή μέρα εξάγει κείμενο από λάθος σελίδα μέσα σε ένα έγγραφο που φτάνει στον πελάτη. Αν τυλίξετε το επίπεδο DA στον δικό σας κώδικα υπηρεσίας, κάντε τη μετάφραση αδύνατο να παραλειφθεί: δεχτείτε αριθμούς σελίδων στο όριο, καλέστε αμέσως την DAFindPage και περνάτε εσωτερικά μόνο refs
Συγχώνευση εκατοντάδων αρχείων με ονομασμένη λίστα
Για δύο αρχεία, η MergeFiles(First, Second, Output) αρκεί. Η μαζική συναρμολόγηση κλιμακώνεται καλύτερα μέσω λιστών αρχείων: καταχωρίστε τις εισόδους κάτω από ένα όνομα λίστας και μετά συγχωνεύστε τη λίστα σε ένα πέρασμα
Lib.AddToFileList('Statements', 'jan.pdf');
Lib.AddToFileList('Statements', 'feb.pdf');
Lib.AddToFileList('Statements', 'mar.pdf');
Lib.MergeFileList('Statements', 'q1-statements.pdf');
// Επαληθεύστε το αποτέλεσμα με τον φθηνό τρόπο: ξανά direct access
Handle := Lib.DAOpenFileReadOnly('q1-statements.pdf', '');
Writeln('merged pages: ', Lib.DAGetPageCount(Handle));
Lib.DACloseFile(Handle);
Η οικογένεια συγχώνευσης έχει τρεις παραλλαγές, και η διαφορά δεν είναι μόνο η ταχύτητα. Η MergeFileListFast παραλείπει τη διατήρηση του δέντρου δομής· η MergeFileListStrict επιβάλλει αυστηρή λειτουργία· η εκδοχή χωρίς κατάληξη είναι η ισορροπημένη προεπιλογή. Ο λειτουργικός κανόνας που προκύπτει: αν κάποια είσοδος είναι Tagged PDF του οποίου η δομή προσβασιμότητας πρέπει να επιβιώσει, με προφανή περίπτωση οτιδήποτε παράγεται για PDF/UA, καταφύγετε στην προεπιλεγμένη ή στη Strict παραλλαγή, επειδή η Fast απορρίπτει σιωπηλά το δέντρο δομής. Για απλά αρχεία σαρώσεων χωρίς tagging, η Fast είναι δωρεάν επίδοση. Αποφασίστε ανά ροή εργασίας, όχι ανά διάθεση προγραμματιστή, και καταγράψτε στο ημερολόγιο της εργασίας ποια παραλλαγή χρησιμοποιήθηκε
Διαίρεση χωρίς φόρτωση: εξαγωγή περιοχών
Η διαίρεση ακολουθεί την ίδια φιλοσοφία της μη φόρτωσης. Η ExtractFilePages(InputFileName, Password, OutputFileName, RangeList) τραβά μια περιοχή σελίδων κατευθείαν από αρχείο σε αρχείο, με λίστα περιοχών όπως '1-500', '501-1000' ή επιλογές χωρισμένες με κόμμα, και η πηγή δεν γίνεται ποτέ δέντρο εγγράφου. Όταν ένα έγγραφο είναι ήδη φορτωμένο για άλλους λόγους, η ExtractPageRanges παράγει ένα νέο έγγραφο στη μνήμη από το τρέχον, ενώ η CopyPageRanges μεταφέρει περιοχές από άλλο φορτωμένο έγγραφο μέσω ID. Για διαίρεση ανά κατάσταση λογαριασμού σε ενοποιημένες ροές εκτύπωσης, η μορφή από αρχείο σε αρχείο είναι εκείνη που εμποδίζει μια είσοδο 4 GB να φουσκώσει ποτέ μέσα στη RAM
Αρχεία που λένε ψέματα για τη γεωμετρία τους
Οι ροές μεγάλων αρχείων συναντούν κατεστραμμένα αρχεία με συχνότητα που οι ροές μικρών αρχείων δεν βλέπουν ποτέ, απλώς επειδή οι είσοδοι περνούν από περισσότερα συστήματα. Δύο μορφές αστοχίας αξίζουν ρητό χειρισμό
Πρώτον, οι μετατοπισμένες κεφαλίδες. Οι πύλες ηλεκτρονικού ταχυδρομείου και οι print spoolers ενίοτε προσθέτουν bytes στην αρχή ενός PDF, οπότε ο δείκτης %PDF δεν βρίσκεται πια στη μετατόπιση 0 και κάθε μετατόπιση xref μέσα στο αρχείο είναι λανθασμένη κατά το ίδιο ποσό. Ο streaming reader το εντοπίζει και το εκθέτει (DAShiftedHeader στο επίπεδο των επίπεδων κλήσεων, ShiftedHeader στην TSmartPDFReader) και έπειτα το αντισταθμίζει κατά τις αναγνώσεις. Η αυτοσχέδια αριθμητική μετατοπίσεων συνήθως δεν το κάνει, γι' αυτό και το «δουλεύει σε κάθε αρχείο που παράγουμε, αποτυγχάνει σε αρχεία από τον πελάτη X» είναι το κλασικό σύμπτωμα
Δεύτερον, οι χαλασμένοι πίνακες διασταυρούμενων αναφορών. Η DACopyFile(InputFileName, OutputFileName, PageCount) στέλνει ολόκληρο το αρχείο σε ένα νέο αντίγραφο ενώ ανακατασκευάζει το xref, επιστρέφοντας ως παράπλευρο προϊόν τον αριθμό των σελίδων. Το να την τρέχετε ως στάδιο κανονικοποίησης μπροστά από έναν ιδιότροπο επόμενο καταναλωτή μετατρέπει μια ολόκληρη κατηγορία διαλείπουσων αποτυχιών ανάλυσης σε ένα προβλέψιμο βήμα επιδιόρθωσης. Και όταν οι δικές σας τροποποιήσεις χρειάζονται αποθήκευση, η DAAppendFile τις γράφει ως incremental update, προσθέτοντας μια νέα αναθεώρηση αντί να ξαναγράφει gigabyte, πράγμα που κρατά το κόστος αποθήκευσης ανάλογο με την αλλαγή και όχι με το αρχείο
Λεπτομέρειες παράδοσης: γραμμικοποίηση και σύνθεση
Δύο συγγενείς δυνατότητες ολοκληρώνουν μια ροή μεγάλων αρχείων. Όταν η συναρμολογημένη έξοδος σερβίρεται μέσω HTTP για προβολή μέσα στον browser, η LinearizeFile την αναδιοργανώνει για streaming με byte-range ώστε η πρώτη σελίδα να εμφανίζεται προτού ολοκληρωθεί η λήψη του υπόλοιπου πακέτου των 500 MB. Τρέξτε την ως τελικό στάδιο, μετά από κάθε συγχώνευση, επειδή οποιαδήποτε μεταγενέστερη τροποποίηση απογραμμικοποιεί ξανά το αρχείο. Και όταν τα πακέτα χρειάζονται σύνθεση αντί για απλή συνένωση — ας πούμε ένα εξώφυλλο σφραγισμένο πίσω από κάθε κατάσταση λογαριασμού ή δύο σελίδες πηγής τοποθετημένες σε ένα φύλλο εξόδου — η DACapturePage μετατρέπει οποιαδήποτε σελίδα σε επαναχρησιμοποιήσιμο πρότυπο που η DADrawCapturedPage τοποθετεί σε μια σελίδα προορισμού σε αυθαίρετο ορθογώνιο, πάντοτε χωρίς πλήρη φόρτωση εγγράφου στην πηγή των πολλών gigabyte
Όρια και τι παραμένει μόνο για ανάγνωση
Η ίδια η μορφή εξαντλεί τον χώρο της πολύ πριν από το Direct Access. Οι μετατοπίσεις είναι Int64 σε όλη τη διαδρομή του επιπέδου DA, οπότε τα πραγματικά ταβάνια είναι ο διαθέσιμος δίσκος και το δεκαψήφιο πεδίο μετατόπισης xref των κλασικών (μη ροϊκών) πινάκων διασταυρούμενων αναφορών. Τα αρχεία σαρώσεων πολλών gigabyte είναι στην πράξη κάτι το συνηθισμένο, και η μνήμη παραμένει φραγμένη ανεξάρτητα από το μέγεθος του αρχείου, επειδή τα αντικείμενα διαβάζονται μόνο όταν τα ζητά κάποια κλήση
Δύο ερωτήματα προκύπτουν αρκετά συχνά ώστε να απαντηθούν ευθέως. Η συγχώνευση μέσω της προεπιλεγμένης διαδρομής μεταφέρει μαζί τη δομή του εγγράφου, οπότε οι σελιδοδείκτες και οι σύνδεσμοι επιβιώνουν· η παραλλαγή Fast είναι εκείνη που ανταλλάσσει το δέντρο δομής με ταχύτητα, που είναι και ο μοναδικός λόγος να τη φυλάτε για εισόδους χωρίς tagging. Η ασφαλής συνήθεια είναι να ανοίγετε τη συγχωνευμένη έξοδο, να διατρέχετε το outline της και να ελέγχετε δειγματοληπτικά μερικούς εσωτερικούς συνδέσμους προτού την παραδώσετε. Όσο για την επεξεργασία: υπάρχει μια χρήσιμη μέση οδός ανάμεσα στην εξέταση μόνο για ανάγνωση και την πλήρη φόρτωση. Οι λειτουργίες σε επίπεδο σελίδας δουλεύουν απευθείας πάνω στο handle — μεταξύ αυτών οι DARotatePage, DAMovePage και DAHidePage — μαζί με τις αναγνώσεις πεδίων φόρμας, και η DAAppendFile διατηρεί αυτές τις τροποποιήσεις ως incremental αναθεώρηση. Η επεξεργασία σε επίπεδο περιεχομένου, οτιδήποτε ξαναγράφει τους τελεστές σήμανσης μέσα σε μια σελίδα, ανήκει ακόμη στο επίπεδο του πλήρους εγγράφου
Σχετικά άρθρα
Αν η συγχωνευμένη έξοδός σας πρέπει να παραμείνει προσβάσιμη, το υπόβαθρο για το δέντρο δομής καλύπτεται στο άρθρο για την προσβασιμότητα Tagged PDF, το οποίο εξηγεί ακριβώς τι θα απέρριπτε η παραλλαγή συγχώνευσης Fast. Για την εξαγωγή περιεχομένου από τις περιοχές που διαιρείτε, δείτε τον οδηγό εξαγωγής κειμένου, εικόνων και γραμματοσειρών
Η πλήρης λίστα συναρτήσεων Direct Access συνοδεύει τη βιβλιοθήκη· οι εκδόσεις και οι δοκιμαστικές λήψεις βρίσκονται στη σελίδα προϊόντος PDF Library for Delphi