Το σφάλμα λέει Please load the document before using BeginDoc, και σχεδόν πάντα εμφανίζεται τη δεύτερη φορά. Το πρώτο έγγραφο γράφεται μια χαρά. Έπειτα ζητείται από την ίδια παρουσία THotPDF να ξεκινήσει ένα δεύτερο, το BeginDoc εγείρει εξαίρεση, και το μήνυμα δείχνει προς τη φόρτωση εγγράφου, που είναι το αντίθετο από αυτό που προσπαθεί να κάνει ο κώδικας. Η ασυμφωνία ανάμεσα στο σύμπτωμα και στο μήνυμα είναι που κάνει αυτό το θέμα να επιμένει. Το πραγματικό αντικείμενο είναι ο κύκλος ζωής του στοιχείου, και μόλις αυτό μπει στη θέση του το σφάλμα παύει να είναι μυστήριο

Μια παρουσία THotPDF είναι ένα έγγραφο, όχι εργοστάσιο εγγράφων
Το δελεαστικό νοητικό μοντέλο είναι ότι το THotPDF είναι ένα αντικείμενο υπηρεσίας που στήνετε μία φορά και του ταΐζετε έγγραφα, όπως θα κρατούσατε ανοιχτή μια σύνδεση βάσης δεδομένων και θα περνούσατε ερώτημα μετά από ερώτημα μέσα από αυτήν. Δεν είναι αυτό. Μια παρουσία μοντελοποιεί ένα μοναδικό έγγραφο υπό κατασκευή, και η εσωτερική της μηχανή καταστάσεων κουβαλά την υπόθεση ότι διανύει τη διαδρομή μία φορά: από το κενό, μέσα από ένα ανοιχτό έγγραφο, ως ένα αποθηκευμένο αρχείο. Το BeginDoc ανοίγει αυτή τη διαδρομή και σημειώνει την παρουσία ως να έχει έγγραφο σε εξέλιξη. Το EndDoc σειριοποιεί τα πάντα στο FileName και το κλείνει. Καλώντας ξανά BeginDoc στην ίδια τελειωμένη παρουσία της ζητάτε να ξαναμπεί σε μια κατάσταση από την οποία ποτέ δεν βγήκε καθαρά, και ο φύλακας που πυροδοτείται είναι εκείνος του οποίου το μήνυμα τυχαίνει να αναφέρει φόρτωση, επειδή εσωτερικά οι συνθήκες «έτοιμο να ξεκινήσει» και «έχει φορτωμένο έγγραφο» ελέγχονται μαζί
Άρα το μήνυμα είναι παραπλανητικό, όμως ο φύλακας κάνει τη δουλειά του. Αρνείται να σας αφήσει να ξεκινήσετε φρέσκο έγγραφο πάνω σε ένα στοιχείο που ακόμη πιστεύει ότι βρίσκεται στα μισά ενός εγγράφου. Η λύση δεν είναι να νικήσετε τον φύλακα. Είναι να σταματήσετε να επαναχρησιμοποιείτε μια εξαντλημένη παρουσία
Ο κύκλος ζωής, με τη σειρά που πρέπει να συμβεί
Κάθε έγγραφο που γράφει το HotPDF από το μηδέν ακολουθεί τους ίδιους τέσσερις χτύπους, και η σειρά δεν είναι διαπραγματεύσιμη. Το Create δεσμεύει το στοιχείο. Το BeginDoc ανοίγει το έγγραφο και καθηλώνει τις δομικές επιλογές, οπότε οτιδήποτε επηρεάζει ολόκληρο το αρχείο (μέγεθος σελίδας, συμπίεση, κρυπτογράφηση, όνομα αρχείου εξόδου) πρέπει να οριστεί ανάμεσα στο Create και το BeginDoc. Μετά σχεδιάζετε. Μετά το EndDoc γράφει τα bytes στον δίσκο. Το Free αποδεσμεύει την παρουσία. Οι κλήσεις σχεδίασης που τοποθετούνται πριν από το BeginDoc δεν έχουν σελίδα να προσγειωθούν· οι ιδιότητες ολόκληρου του εγγράφου που ανατίθενται μετά από αυτό αγνοούνται χωρίς παράπονο
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'invoice.pdf';
Pdf.BeginDoc; // ανοίγει το έγγραφο
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Invoice 2026-042');
Pdf.EndDoc; // γράφει το invoice.pdf και το κλείνει
finally
Pdf.Free; // μία παρουσία, ένα έγγραφο
end;
end;
Διαβάστε το ως τη μονάδα εργασίας. Ένα Create, ένα BeginDoc, ένα EndDoc, ένα Free, ένα αρχείο στον δίσκο. Τη στιγμή που θέλετε δεύτερο αρχείο, ξεκινάτε νέα μονάδα εργασίας, πράγμα που σημαίνει νέα παρουσία
Τι πρέπει να σημαίνει η «επαναχρησιμοποίηση»: φρέσκια παρουσία ανά αρχείο
Η έκδοση που σπάει προσπαθεί να είναι φειδωλή με τη δέσμευση: φτιάχνει το στοιχείο μία φορά, κάνει βρόχο πάνω σε μια παρτίδα, και καλεί BeginDoc και EndDoc μέσα στον βρόχο. Η δεύτερη επανάληψη εγείρει εξαίρεση. Η έκδοση που δουλεύει αντιμετωπίζει κάθε έξοδο ως δικό της βραχύβιο αντικείμενο, και το κόστος δέσμευσης ενός στοιχείου είναι ασήμαντο δίπλα στη δουλειά της διάταξης και της σειριοποίησης ενός PDF, οπότε δεν υπάρχει τίποτα να κερδίσετε αποθησαυρίζοντας την παρουσία
procedure WriteBatch(const Names: TArray<string>);
var
I: Integer;
Pdf: THotPDF;
begin
for I := 0 to High(Names) do
begin
Pdf := THotPDF.Create(nil); // νέα παρουσία σε κάθε πέρασμα
try
Pdf.FileName := Names[I] + '.pdf';
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 12);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Statement for ' + Names[I]);
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
end;
Το try/finally που κάθεται μέσα στον βρόχο είναι το κομμάτι που αξίζει να υπερασπιστείτε σε έναν έλεγχο κώδικα. Αν το BeginDoc ή κάποια κλήση σχεδίασης εγείρει εξαίρεση στα μισά ενός εγγράφου, η παρουσία εκείνης της επανάληψης αποδεσμεύεται πριν ξεκινήσει η επόμενη, οπότε μία κακή εγγραφή δεν αφήνει ξεκρέμαστο ένα μισοφτιαγμένο στοιχείο ούτε δηλητηριάζει την υπόλοιπη εκτέλεση. Βγάλτε το Create έξω πάνω από τον βρόχο για να «βελτιστοποιήσετε» και επιστρέψατε στο αρχικό σφάλμα, τώρα ντυμένο με βρόχο παρτίδας
Η τροποποίηση υπάρχοντος αρχείου είναι διαφορετικό σημείο εισόδου
Υπάρχει μια δεύτερη ανάγνωση της «επαναχρησιμοποίησης» που είναι απολύτως θεμιτή: δεν θέλετε κενό έγγραφο, θέλετε να ανοίξετε ένα PDF που υπάρχει ήδη και να το αλλάξετε. Αυτή η διαδρομή δεν περνά καθόλου από το BeginDoc, και ακριβώς γι αυτό το μήνυμα σφάλματος αναφέρει φόρτωση. Φορτώνετε το αρχείο, το επεξεργάζεστε, και το αποθηκεύετε με όποιο όνομα διαλέξετε
var
Pdf: THotPDF;
PageCount: Integer;
begin
Pdf := THotPDF.Create(nil);
try
PageCount := Pdf.LoadFromFile('contract.pdf');
if PageCount > 0 then
begin
Pdf.CurrentPage.SetFont('Arial', [fsBold], 10);
Pdf.CurrentPage.TextOut(40, 30, 0, 'REVIEWED');
Pdf.SaveLoadedDocument('contract-reviewed.pdf');
end;
finally
Pdf.Free;
end;
end;
Το LoadFromFile επιστρέφει το πλήθος σελίδων, και τιμή μηδέν ή μικρότερη σημαίνει ότι η φόρτωση απέτυχε, οπότε αξίζει να το ελέγξετε πριν αγγίξετε το CurrentPage. Το ζευγάρωμα έχει σημασία: ένα έγγραφο που ανοίξατε με LoadFromFile αποθηκεύεται με SaveLoadedDocument, όχι με το ζεύγος BeginDoc/EndDoc, που ανήκει σε έγγραφα τα οποία συντάσσετε από το τίποτα. Η ανάμειξη των δύο είναι ο πιο συνηθισμένος τρόπος να μπερδέψετε την ίδια μηχανή καταστάσεων που παρήγαγε το αρχικό σφάλμα. Κρατήστε τις δύο ροές νοητά χωριστές: BeginDoc ... EndDoc δημιουργεί, LoadFromFile ... SaveLoadedDocument επεξεργάζεται
Το πρόβλημα κλειδώματος αρχείου είναι υπαρκτό, και η απάντηση δεν είναι να σκοτώνετε παράθυρα προβολής
Το σφάλμα επαναχρησιμοποίησης συχνά ταξιδεύει μαζί με ένα δεύτερο παράπονο, και τα δύο μπερδεύονται επειδή εμφανίζονται στην ίδια ροή εργασίας αναδημιουργίας αρχείου. Ένας χρήστης ανοίγει το PDF που μόλις παραγάγατε, το αφήνει ανοιχτό στο Acrobat ή στο Foxit, και μετά πυροδοτεί μια επανακατασκευή. Το EndDoc προσπαθεί να γράψει στην ίδια διαδρομή, το λειτουργικό σύστημα αρνείται επειδή το πρόγραμμα προβολής κρατά κοινή χρήση ανάγνωσης που μπλοκάρει τους εγγραφείς, και παίρνετε αποτυχία άρνησης πρόσβασης. Αυτό είναι πράγματι ζήτημα κλειδώματος αρχείων των Windows και όχι ζήτημα κατάστασης του στοιχείου, και αξίζει πραγματική απάντηση αντί για παράκαμψη
Η παράκαμψη που κυκλοφορεί, δηλαδή η απαρίθμηση των παραθύρων ανώτατου επιπέδου και η αποστολή WM_CLOSE σε ό,τι έχει τίτλο που μοιάζει με πρόγραμμα προβολής PDF, είναι λάθος ένστικτο. Απλώνει το χέρι πέρα από τα όρια διεργασιών για να κλείσει παράθυρα που δεν ανήκουν στο πρόγραμμά σας, μαντεύει τα προγράμματα προβολής από το κείμενο του τίτλου, και μπορεί να πετάξει τις μη αποθηκευμένες σημειώσεις ενός χρήστη χωρίς να ρωτήσει. Θεωρήστε ολόκληρη αυτή την προσέγγιση ύποπτη. Η αξιόπιστη λύση είναι να μη γράφετε ποτέ σε διαδρομή που μπορεί να κρατά άλλη διεργασία. Σειριοποιήστε σε προσωρινό αρχείο στον ίδιο κατάλογο, και μετά ανταλλάξτε το στη θέση του με ατομική μετονομασία μόλις πετύχει το EndDoc. Αν κάποιο πρόγραμμα προβολής κρατά ακόμη ανοιχτό το παλιό αρχείο, η μετονομασία είτε πετυχαίνει καθαρά είτε αποτυγχάνει θορυβωδώς, και εσείς εμφανίζετε ένα σαφές μήνυμα αντί να παλεύετε με το κλείδωμα
uses
System.SysUtils, System.IOUtils;
procedure WritePdfAtomically(const FinalPath: string);
var
Pdf: THotPDF;
TempPath: string;
begin
// Προσωρινό αρχείο στον ΙΔΙΟ κατάλογο με τον στόχο: μια μετονομασία μέσα
// σε έναν τόμο NTFS ανταλλάσσει το όνομα ατομικά, ενώ μια μετακίνηση
// ανάμεσα σε τόμους εκφυλίζεται σε αντιγραφή και διαγραφή και χάνει την εγγύηση
TempPath := TPath.Combine(TPath.GetDirectoryName(FinalPath),
TGUID.NewGuid.ToString + '.pdf.tmp');
try
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := TempPath;
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Invoice 2026-042');
Pdf.EndDoc; // εδώ το προσωρινό αρχείο είναι πλήρες στον δίσκο
finally
Pdf.Free;
end;
// Ανταλλαγή στη θέση του. Το TFile.Move αρνείται να αντικαταστήσει, οπότε
// καθαρίστε πρώτα έναν παλιό στόχο· αν κάποιο πρόγραμμα προβολής κρατά
// ακόμη το παλιό αρχείο, η διαγραφή αποτυγχάνει θορυβωδώς πριν αγγιχτούν τα καλά bytes
if TFile.Exists(FinalPath) then
TFile.Delete(FinalPath);
TFile.Move(TempPath, FinalPath); // ή: RenameFile(TempPath, FinalPath)
except
if TFile.Exists(TempPath) then
TFile.Delete(TempPath); // ποτέ μην αφήνετε ξεκρέμαστο μισογραμμένο προσωρινό αρχείο
raise;
end;
end;
Δύο ειλικρινείς υποσημειώσεις για αυτόν τον κώδικα. Το TFile.Move και η κλασική RenameFile αντιστοιχούν και τα δύο στην ίδια μετονομασία των Windows, η οποία είναι ατομική μόνο όταν η πηγή και ο προορισμός βρίσκονται στον ίδιο τόμο, και ακριβώς γι αυτό το προσωρινό αρχείο πηγαίνει στον κατάλογο προορισμού και όχι στο TPath.GetTempPath. Και το ζεύγος διαγραφή-μετά-μετακίνηση δεν είναι το ίδιο ένα ατομικό βήμα: υπάρχει ένα σύντομο παράθυρο στο οποίο δεν υπάρχει κανένα από τα δύο αρχεία. Για μια επιτραπέζια εφαρμογή που αναδημιουργεί μια αναφορά αυτό το παράθυρο δεν έχει σημασία· όσοι αναγνώστες χρειάζονται ισχυρότερο συμβόλαιο στον ίδιο τόμο μπορούν να καλέσουν απευθείας τις Win32 ReplaceFile ή MoveFileEx με MOVEFILE_REPLACE_EXISTING, που συμπτύσσουν την ανταλλαγή σε μία κλήση
Για έναν διακομιστή μεγάλου όγκου που αναδημιουργεί έγγραφα συνεχώς, η καθαρότερη πειθαρχία είναι να γράφετε κάθε έξοδο με μοναδικό όνομα (μια χρονοσφραγίδα ή ένα job id) ώστε δύο εκτελέσεις να μη διεκδικούν ποτέ την ίδια διαδρομή, και να αφήνετε μια ξεχωριστή πολιτική διατήρησης να καθαρίζει τα παλιά αρχεία. Το μοτίβο είναι μία γραμμή πειθαρχίας ονοματοδοσίας ανά αίτημα
// Μία διαδρομή εξόδου ανά αίτημα: δύο ταυτόχρονες εργασίες δεν μπορούν ποτέ
// να διεκδικήσουν το ίδιο όνομα, οπότε ούτε χορός μετονομασίας ούτε κλείδωμα να χαθεί
OutName := Format('statement-%s-%s.pdf',
[CustomerId, TGUID.NewGuid.ToString.Trim(['{', '}'])]);
Pdf.FileName := TPath.Combine(OutputDir, OutName);
Ένα request id ή ένα job id δουλεύει εξίσου καλά με το GUID όταν το περιβάλλον γύρω σας ήδη σας δίνει ένα, και κάνει το όνομα αρχείου ιχνηλατήσιμο πίσω σε μια γραμμή καταγραφής χωρίς κόστος. Έτσι κι αλλιώς η αρχή είναι η ίδια: σχεδιάστε ώστε το αρχείο που γράφετε να είναι αποκλειστικά δικό σας τη στιγμή που το γράφετε. Το κλείδωμα εξαφανίζεται όχι επειδή αναγκάσατε ένα παράθυρο να κλείσει αλλά επειδή τίποτε άλλο δεν αγγίζει τα bytes
Το σχήμα της λύσης
Ξεγυμνώστε τα δύο προβλήματα ως τη ρίζα τους και θα δείτε ότι και τα δύο αφορούν τον σεβασμό των ορίων. Το σφάλμα της μηχανής καταστάσεων θέλει να τιμήσετε το όριο της παρουσίας: ένα THotPDF, ένα έγγραφο, και μετά αφήστε το να φύγει και φτιάξτε άλλο. Το σφάλμα κλειδώματος αρχείου θέλει να τιμήσετε το όριο του αρχείου: γράψτε εκεί που τίποτε άλλο δεν διαβάζει, και μετά μετακινήστε το αποτέλεσμα στη θέση του. Κανένα από τα δύο δεν απαιτεί μπάλωμα της βιβλιοθήκης ούτε σενάρια πάνω στην επιφάνεια εργασίας. Και τα δύο προκύπτουν από το να αντιμετωπίζετε κάθε έγγραφο ως αυτοτελή μονάδα εργασίας, φτιαγμένη από την αρχή, γραμμένη καθαρά και αποδεσμευμένη, που είναι το ίδιο μοτίβο που κάνει προβλέψιμο και το υπόλοιπο στοιχείο
Οι κλήσεις BeginDoc, EndDoc, LoadFromFile και SaveLoadedDocument που παρουσιάζονται εδώ αποτελούν μέρος του HotPDF Delphi Component για Delphi και C++Builder