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

Επαναχρησιμοποίηση μιας Παρουσίας THotPDF σε Πολλά Έγγραφα στο Delphi

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

Κύκλος ζωής εγγράφου THotPDF που δείχνει Create, BeginDoc, EndDoc και Free ανά αρχείο εξόδου
Μια παρουσία (instance) THotPDF αντιστοιχίζεται (maps) σε ένα έγγραφο: Create, BeginDoc, σχεδίαση (draw), EndDoc, Free.

Μια παρουσία THotPDF είναι ένα έγγραφο, όχι ένα εργοστάσιο εγγράφων

Το δελεαστικό νοητικό μοντέλο είναι ότι το THotPDF είναι ένα αντικείμενο υπηρεσίας (service object) που το εκκινείτε μία φορά και του τροφοδοτείτε έγγραφα, με τον τρόπο που μπορεί να κρατούσατε μια σύνδεση βάσης δεδομένων ανοιχτή και να εκτελούσατε ερώτημα (query) μετά από ερώτημα μέσω αυτής. Δεν είναι αυτό. Μια παρουσία (instance) μοντελοποιεί ένα μόνο έγγραφο που κατασκευάζεται, και η εσωτερική του μηχανή κατάστασης (state machine) φέρει την υπόθεση ότι διανύει τη διαδρομή μία φορά: από άδειο, μέσω ενός ανοιχτού εγγράφου, σε ένα αποθηκευμένο αρχείο. Το BeginDoc ανοίγει αυτήν τη διαδρομή και επισημαίνει την παρουσία ως έχουσα ένα έγγραφο σε εξέλιξη. Το EndDoc σειριοποιεί (serializes) τα πάντα στο FileName και το κλείνει. Η εκ νέου κλήση του BeginDoc στην ίδια ολοκληρωμένη (finished) παρουσία της ζητά να εισέλθει ξανά σε μια κατάσταση που ποτέ δεν άφησε καθαρά (cleanly), και ο φρουρός (guard) που ενεργοποιείται είναι αυτός του οποίου το μήνυμα τυχαίνει να αναφέρει (mention) τη φόρτωση, επειδή εσωτερικά οι συνθήκες "έτοιμο για έναρξη" και "έχει φορτωμένο έγγραφο" ελέγχονται μαζί

Έτσι, το μήνυμα είναι παραπλανητικό, αλλά ο φρουρός κάνει τη δουλειά του. Αρνείται να σας αφήσει να ξεκινήσετε ένα νέο (fresh) έγγραφο πάνω σε ένα στοιχείο που εξακολουθεί να πιστεύει ότι βρίσκεται στα μέσα του εγγράφου (mid-document). Η διόρθωση δεν είναι να νικήσετε (defeat) τον φρουρό. Είναι να σταματήσετε να επαναχρησιμοποιείτε (reusing) μια εξαντλημένη (spent) παρουσία

Ο κύκλος ζωής, με τη σειρά που πρέπει να συμβεί

Κάθε έγγραφο που γράφει το HotPDF από την αρχή ακολουθεί τους ίδιους τέσσερις παλμούς (beats), και η σειρά είναι αδιαπραγμάτευτη. Το Create εκχωρεί το στοιχείο (component). Το BeginDoc ανοίγει το έγγραφο και σταθεροποιεί (fixes) τις δομικές (structural) επιλογές, επομένως οτιδήποτε επηρεάζει ολόκληρο το αρχείο (μέγεθος σελίδας, συμπίεση, κρυπτογράφηση, όνομα αρχείου εξόδου) πρέπει να ρυθμιστεί μεταξύ Create και BeginDoc. Μετά σχεδιάζετε (draw). Στη συνέχεια το EndDoc γράφει τα bytes στο δίσκο. Το Free απελευθερώνει την παρουσία. Οι κλήσεις σχεδίασης (Drawing calls) που τοποθετούνται πριν από το BeginDoc δεν έχουν σελίδα για να προσγειωθούν· οι ιδιότητες ολόκληρου-του-εγγράφου που εκχωρούνται (assigned) μετά από αυτό αγνοούνται χωρίς παράπονο

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'invoice.pdf';
    Pdf.BeginDoc;                        // opens the document
    Pdf.CurrentPage.SetFont('Arial', [], 11);
    Pdf.CurrentPage.TextOut(50, 760, 0, 'Invoice 2026-042');
    Pdf.EndDoc;                          // writes invoice.pdf, closes it out
  finally
    Pdf.Free;                            // one instance, one document
  end;
end;

Διαβάστε το αυτό ως τη μονάδα εργασίας (unit of work). Ένα Create, ένα BeginDoc, ένα EndDoc, ένα Free, ένα αρχείο στο δίσκο. Τη στιγμή που θέλετε ένα δεύτερο αρχείο, ξεκινάτε μια νέα μονάδα εργασίας, που σημαίνει μια νέα παρουσία (instance)

Τι θα έπρεπε να σημαίνει "επαναχρησιμοποίηση": μια νέα (fresh) παρουσία ανά αρχείο

Η έκδοση που χαλάει (breaks) προσπαθεί να είναι φειδωλή με την εκχώρηση: κατασκευάστε το στοιχείο μία φορά, κάντε βρόχο (loop) πάνω σε μια παρτίδα (batch), καλέστε BeginDoc και EndDoc μέσα στο βρόχο. Η δεύτερη επανάληψη (iteration) ρίχνει (throws) σφάλμα. Η έκδοση που λειτουργεί αντιμετωπίζει κάθε έξοδο (output) ως το δικό της βραχύβιο (short-lived) αντικείμενο, και το κόστος εκχώρησης (allocation cost) της δημιουργίας ενός στοιχείου είναι ασήμαντο δίπλα στο έργο της διάταξης (laying out) και σειριοποίησης ενός PDF, επομένως δεν υπάρχει τίποτα να σωθεί με την αποθησαύριση (hoarding) της παρουσίας

procedure WriteBatch(const Names: TArray<string>);
var
  I: Integer;
  Pdf: THotPDF;
begin
  for I := 0 to High(Names) do
  begin
    Pdf := THotPDF.Create(nil);         // new instance each pass
    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 που κάθεται μέσα στο βρόχο (loop) είναι το τμήμα που αξίζει να υπερασπιστείτε σε μια ανασκόπηση (review). Αν το BeginDoc ή οποιαδήποτε κλήση σχεδίασης εγείρει (raises) σφάλμα (raises partway through) σε ένα έγγραφο, η παρουσία εκείνης της επανάληψης απελευθερώνεται (freed) ούτως ή άλλως πριν ξεκινήσει η επόμενη, έτσι μια κακή εγγραφή δεν αφήνει αβοήθητο (strand) ένα μισοφτιαγμένο στοιχείο και δεν δηλητηριάζει την υπόλοιπη εκτέλεση (run). Τραβήξτε το Create έξω, πάνω από το βρόχο για "βελτιστοποίηση" (optimize) και επιστρέφετε στο αρχικό σφάλμα, φορώντας τώρα ένα βρόχο παρτίδας (batch loop)

Η τροποποίηση ενός υπάρχοντος αρχείου είναι ένα διαφορετικό σημείο εισόδου

Υπάρχει μια δεύτερη ανάγνωση της "επαναχρησιμοποίησης" που είναι απολύτως θεμιτή (legitimate): δεν θέλετε ένα κενό έγγραφο, θέλετε να ανοίξετε ένα PDF που ήδη υπάρχει και να το αλλάξετε. Αυτή η διαδρομή δεν περνά καθόλου μέσα από το BeginDoc, που είναι ακριβώς ο λόγος που το μήνυμα σφάλματος ονομάζει τη φόρτωση. Φορτώνετε (load) το αρχείο, το επεξεργάζεστε και το αποθηκεύετε με όποιο όνομα επιλέξετε

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, το οποίο ανήκει σε έγγραφα που συντάσσετε από το μηδέν (nothing). Η ανάμειξη των δύο είναι ο πιο συνηθισμένος τρόπος για να μπερδέψετε την ίδια μηχανή κατάστασης που παρήγαγε το αρχικό σφάλμα. Κρατήστε τις δύο ροές (flows) νοητά (mentally) χωριστές: BeginDoc ... EndDoc δημιουργεί, LoadFromFile ... SaveLoadedDocument επεξεργάζεται

Το πρόβλημα κλειδώματος αρχείου είναι πραγματικό, και η απάντηση δεν είναι να σκοτώνετε τα παράθυρα προβολής

Το σφάλμα επαναχρησιμοποίησης συχνά ταξιδεύει μαζί με ένα δεύτερο παράπονο, και τα δύο μπερδεύονται μεταξύ τους επειδή βγαίνουν στην επιφάνεια στην ίδια ροή εργασίας (workflow) "αναδημιουργίας του αρχείου" (regenerate-the-file). Ένας χρήστης ανοίγει το PDF που μόλις δημιουργήσατε, το αφήνει ανοιχτό στο Acrobat ή στο Foxit, και στη συνέχεια πυροδοτεί (triggers) μια ανακατασκευή (rebuild). Το EndDoc προσπαθεί να γράψει την ίδια διαδρομή, το λειτουργικό σύστημα αρνείται (refuses) επειδή το πρόγραμμα προβολής (viewer) διατηρεί (holds) ένα μερίδιο ανάγνωσης (read share) που εμποδίζει (blocks) τους συγγραφείς (writers), και λαμβάνετε μια αποτυχία δεν-επιτρέπεται-η-πρόσβαση (access-denied). Αυτό είναι πραγματικά ένα ζήτημα κλειδώματος αρχείων (file-locking) των Windows παρά ένα ζήτημα κατάστασης-στοιχείου (component-state), και του αξίζει μια πραγματική απάντηση αντί για μια λύση παράκαμψης (workaround)

Η λύση παράκαμψης (workaround) που κυκλοφορεί, που απαριθμεί (enumerating) τα παράθυρα ανώτατου επιπέδου (top-level) και δημοσιεύει (posting) το WM_CLOSE σε οτιδήποτε του οποίου ο τίτλος μοιάζει με πρόγραμμα προβολής PDF (PDF viewer), είναι το λάθος ένστικτο (instinct). Φτάνει πέρα (reaches across) από τα όρια της διεργασίας (process boundaries) για να κλείσει παράθυρα που δεν κατέχει το πρόγραμμά σας, μαντεύει τα προγράμματα προβολής από το κείμενο του τίτλου και μπορεί να πετάξει τους μη αποθηκευμένους σχολιασμούς ενός χρήστη χωρίς να ρωτήσει. Αντιμετωπίστε όλη αυτή την προσέγγιση ως οσμή (smell). Η αξιόπιστη (reliable) διόρθωση είναι να μην γράφετε ποτέ σε μια διαδρομή (path) που μπορεί να κρατάει (holding) μια άλλη διεργασία. Σειριοποιήστε (Serialize) σε ένα προσωρινό (temporary) αρχείο στον ίδιο κατάλογο (directory), στη συνέχεια ανταλλάξτε (swap) το στη θέση του με μια ατομική μετονομασία (atomic rename) μόλις πετύχει το EndDoc. Εάν ένα πρόγραμμα προβολής εξακολουθεί να έχει ανοιχτό το παλιό αρχείο, η μετονομασία είτε επιτυγχάνει (succeeds) καθαρά είτε αποτυγχάνει ηχηρά (loudly), και βγάζετε (surface) ένα σαφές μήνυμα αντί να παλεύετε (fighting) με την κλειδαριά (lock)

Για έναν διακομιστή (server) μεγάλου όγκου (high-volume) που αναδημιουργεί (regenerates) έγγραφα συνεχώς, η πιο καθαρή πειθαρχία (discipline) είναι να γράφει κάθε έξοδο κάτω από ένα μοναδικό (unique) όνομα (ένα χρονικό στίγμα / timestamp ή ένα αναγνωριστικό εργασίας / job id) ώστε δύο εκτελέσεις να μην ανταγωνίζονται (contend) ποτέ για μία διαδρομή, και να αφήσει μια ξεχωριστή (separate) πολιτική διατήρησης (retention policy) να καθαρίσει (clean up) τα παλιά αρχεία. Είτε έτσι είτε αλλιώς η αρχή (principle) είναι η ίδια: σχεδιάστε έτσι ώστε το αρχείο που γράφετε να είναι μόνο δικό σας τη στιγμή που το γράφετε. Το κλείδωμα (lock) εξαφανίζεται (disappears) όχι επειδή κλείσατε με το ζόρι (forced shut) ένα παράθυρο, αλλά επειδή τίποτα άλλο δεν αγγίζει (touching) τα byte

Το σχήμα της διόρθωσης (fix)

Αφαιρέστε τα δύο προβλήματα μέχρι τις ρίζες (roots) τους και αφορούν (are about) και τα δύο τον σεβασμό των ορίων (boundaries). Το σφάλμα της μηχανής κατάστασης (state-machine error) θέλει να τιμήσετε (honor) το όριο της παρουσίας (instance boundary): ένα THotPDF, ένα έγγραφο, μετά αφήστε το και φτιάξτε ένα άλλο. Το σφάλμα κλειδώματος αρχείου (file-lock error) θέλει να τιμήσετε το όριο του αρχείου (file boundary): γράψτε εκεί που δεν διαβάζει τίποτα άλλο, μετά μετακινήστε (move) το αποτέλεσμα στη θέση του. Κανένα δεν απαιτεί ενημέρωση (patching) της βιβλιοθήκης ή συγγραφή σεναρίων (scripting) για την επιφάνεια εργασίας (desktop). Και τα δύο προκύπτουν (fall out of) αντιμετωπίζοντας (treating) κάθε έγγραφο ως μια αυτόνομη (self-contained) μονάδα εργασίας, δημιουργημένη από την αρχή, γραμμένη καθαρά (cleanly), και απελευθερωμένη (released), το οποίο είναι το ίδιο μοτίβο που κάνει το υπόλοιπο στοιχείο προβλέψιμο (predictable)

Οι κλήσεις (calls) BeginDoc, EndDoc, LoadFromFile και SaveLoadedDocument που εμφανίζονται εδώ αποτελούν μέρος του Στοιχείου Ελέγχου HotPDF για το Delphi και το C++Builder