Articol tehnic

Reutilizarea unei instanțe THotPDF în Delphi

Eroarea sună Please load the document before using BeginDoc și apare aproape întotdeauna a doua oară. Primul document se scrie fără probleme. Apoi aceleiași instanțe THotPDF i se cere să înceapă un al doilea, BeginDoc ridică excepția, iar mesajul arată spre încărcarea unui document, adică exact opusul a ceea ce încearcă să facă acel cod. Nepotrivirea dintre simptom și mesaj este cea care face ca această problemă să persiste. Subiectul real este ciclul de viață al componentei, iar odată ce acesta se așază, eroarea încetează să mai fie misterioasă

Ciclul de viață al documentului THotPDF, care arată Create, BeginDoc, EndDoc și Free pentru fiecare fișier de ieșire
O instanță THotPDF corespunde unui singur document: Create, BeginDoc, desenare, EndDoc, Free

O instanță THotPDF este un document, nu o fabrică de documente

Modelul mental tentant este că THotPDF ar fi un obiect de serviciu pe care îl porniți o dată și căruia îi dați documente, așa cum ați ține deschisă o conexiune la baza de date și ați rula prin ea interogare după interogare. Nu este așa ceva. O instanță modelează un singur document aflat în construcție, iar mașina ei internă de stări poartă presupunerea că parcurge drumul o singură dată: de la gol, printr-un document deschis, până la un fișier salvat. BeginDoc deschide acel drum și marchează instanța ca având un document în lucru. EndDoc serializează totul în FileName și îl închide. Apelarea din nou a lui BeginDoc pe aceeași instanță deja încheiată îi cere să reintre într-o stare din care nu a ieșit niciodată curat, iar garda care se declanșează este cea al cărei mesaj se întâmplă să pomenească încărcarea, pentru că, pe plan intern, condițiile „gata de început” și „are un document încărcat” sunt verificate împreună

Așadar mesajul induce în eroare, dar garda își face treaba. Ea refuză să vă lase să porniți un document nou peste o componentă care încă se crede în mijlocul unui document. Corectura nu este să învingeți garda, ci să nu mai reutilizați o instanță consumată

Ciclul de viață, în ordinea în care trebuie să se petreacă

Fiecare document pe care HotPDF îl scrie de la zero urmează aceleași patru bătăi, iar ordinea nu este negociabilă. Create alocă componenta. BeginDoc deschide documentul și fixează alegerile structurale, așa că orice afectează întregul fișier (dimensiunea paginii, compresia, criptarea, numele fișierului de ieșire) trebuie setat între Create și BeginDoc. Apoi desenați. Apoi EndDoc scrie octeții pe disc. Free eliberează instanța. Apelurile de desenare puse înainte de BeginDoc nu au nicio pagină pe care să aterizeze; proprietățile la nivel de document atribuite după el sunt ignorate fără nicio plângere

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'invoice.pdf';
    Pdf.BeginDoc;                        // deschide documentul
    Pdf.CurrentPage.SetFont('Arial', [], 11);
    Pdf.CurrentPage.TextOut(50, 760, 0, 'Invoice 2026-042');
    Pdf.EndDoc;                          // scrie invoice.pdf și îl închide
  finally
    Pdf.Free;                            // o instanță, un document
  end;
end;

Citiți asta ca pe unitatea de lucru. Un Create, un BeginDoc, un EndDoc, un Free, un fișier pe disc. În clipa în care vreți un al doilea fișier, începeți o nouă unitate de lucru, ceea ce înseamnă o instanță nouă

Ce ar trebui să însemne „reutilizare”: o instanță nouă pentru fiecare fișier

Varianta care se strică încearcă să fie economă cu alocarea: construiește componenta o dată, parcurge un lot în buclă și apelează BeginDoc și EndDoc în interiorul buclei. A doua iterație aruncă excepția. Varianta care funcționează tratează fiecare ieșire ca pe un obiect propriu, de scurtă durată, iar costul de alocare al creării unei componente este neglijabil pe lângă munca de aranjare și serializare a unui PDF, deci nu se economisește nimic prin păstrarea instanței

procedure WriteBatch(const Names: TArray<string>);
var
  I: Integer;
  Pdf: THotPDF;
begin
  for I := 0 to High(Names) do
  begin
    Pdf := THotPDF.Create(nil);         // instanță nouă la fiecare trecere
    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;

Blocul try/finally așezat în interiorul buclei este partea care merită apărată la o revizuire. Dacă BeginDoc sau vreun apel de desenare ridică o excepție la jumătatea unui document, instanța acelei iterații este totuși eliberată înainte să înceapă următoarea, deci o înregistrare defectă nu lasă în urmă o componentă construită pe jumătate și nu otrăvește restul rulării. Scoateți Create deasupra buclei ca să „optimizați” și v-ați întors la defectul original, acum îmbrăcat într-o buclă de lot

Modificarea unui fișier existent este un alt punct de intrare

Există o a doua citire a „reutilizării”, perfect legitimă: nu vreți un document gol, ci vreți să deschideți un PDF care există deja și să îl schimbați. Acea cale nu trece deloc prin BeginDoc, ceea ce explică exact de ce mesajul de eroare pomenește încărcarea. Încărcați fișierul, îl editați și îl salvați sub ce nume alegeți

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 returnează numărul de pagini, iar o valoare de zero sau mai mică înseamnă că încărcarea a eșuat, așa că merită verificată înainte să atingeți CurrentPage. Perechea contează: un document pe care l-ați deschis cu LoadFromFile se salvează cu SaveLoadedDocument, nu cu perechea BeginDoc/EndDoc, care aparține documentelor create de la zero. Amestecarea celor două este cel mai frecvent mod de a deruta chiar mașina de stări care a produs eroarea inițială. Țineți cele două fluxuri separate în minte: BeginDoc ... EndDoc creează, LoadFromFile ... SaveLoadedDocument editează

Problema blocării fișierului este reală, iar soluția nu este uciderea ferestrelor de vizualizare

Eroarea de reutilizare călătorește adesea cu o a doua plângere, iar cele două se încurcă între ele pentru că apar în același flux de regenerare a fișierului. Un utilizator deschide PDF-ul tocmai produs, îl lasă deschis în Acrobat sau Foxit, apoi declanșează o reconstruire. EndDoc încearcă să scrie aceeași cale, sistemul de operare refuză pentru că vizualizatorul ține o partajare de citire care blochează scriitorii, iar dumneavoastră primiți un eșec de tip acces refuzat. Acesta este cu adevărat o problemă de blocare a fișierelor în Windows, nu una de stare a componentei, și merită un răspuns real în locul unei improvizații

Improvizația care circulă, enumerarea ferestrelor de nivel superior și trimiterea unui WM_CLOSE spre orice fereastră al cărei titlu seamănă cu al unui vizualizator PDF, este instinctul greșit. Ea trece granițele dintre procese ca să închidă ferestre pe care programul dumneavoastră nu le deține, ghicește vizualizatoarele după textul din titlu și poate arunca fără să întrebe adnotările nesalvate ale unui utilizator. Tratați întreaga abordare ca pe un semn rău. Corectura de încredere este să nu scrieți niciodată într-o cale pe care alt proces ar putea să o țină. Serializați într-un fișier temporar din același director, apoi puneți-l la locul lui printr-o redenumire atomică, odată ce EndDoc a reușit. Dacă un vizualizator încă are vechiul fișier deschis, redenumirea fie reușește curat, fie eșuează zgomotos, iar dumneavoastră scoateți la suprafață un mesaj clar în loc să vă luptați cu blocarea

uses
  System.SysUtils, System.IOUtils;

procedure WritePdfAtomically(const FinalPath: string);
var
  Pdf: THotPDF;
  TempPath: string;
begin
  // Fișier temporar în ACELAȘI director cu ținta: o redenumire în interiorul
  // unui volum NTFS schimbă numele atomic, în timp ce o mutare între volume
  // degenerează în copiere plus ștergere și pierde această garanție
  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;                    // aici fișierul temporar este complet pe disc
    finally
      Pdf.Free;
    end;

    // Punere la loc. TFile.Move refuză să suprascrie, deci curățați întâi o
    // țintă veche; dacă un vizualizator încă ține vechiul fișier, ștergerea
    // este cea care eșuează, zgomotos, înainte de a atinge octeții buni
    if TFile.Exists(FinalPath) then
      TFile.Delete(FinalPath);
    TFile.Move(TempPath, FinalPath); // sau: RenameFile(TempPath, FinalPath)
  except
    if TFile.Exists(TempPath) then
      TFile.Delete(TempPath);        // nu lăsați niciodată în urmă un temporar pe jumătate scris
    raise;
  end;
end;

Două note cinstite la acel cod. TFile.Move și clasicul RenameFile se mapează amândouă pe aceeași redenumire Windows, care este atomică doar când sursa și destinația stau pe același volum, și tocmai de aceea fișierul temporar ajunge în directorul de destinație, nu în TPath.GetTempPath. Iar perechea ștergere-apoi-mutare nu este ea însăși un singur pas atomic: există o scurtă fereastră în care niciun fișier nu există. Pentru o aplicație desktop care regenerează un raport, acea fereastră este irelevantă; cititorii care au nevoie de un contract mai puternic pe același volum pot apela direct funcțiile Win32 ReplaceFile sau MoveFileEx cu MOVEFILE_REPLACE_EXISTING, ceea ce comprimă schimbul într-un singur apel

Pentru un server cu volum mare, care regenerează documente în permanență, disciplina mai curată este să scrieți fiecare ieșire sub un nume unic (un marcaj de timp sau un id de sarcină), astfel încât două rulări să nu se dispute niciodată o cale, și să lăsați o politică de retenție separată să curețe fișierele vechi. Tiparul este o singură linie de disciplină a denumirii pentru fiecare cerere

// O singură cale de ieșire pentru fiecare cerere: două sarcini concurente nu
// se pot disputa niciodată același nume, deci fără dans de redenumire și fără blocare pierdută
OutName := Format('statement-%s-%s.pdf',
  [CustomerId, TGUID.NewGuid.ToString.Trim(['{', '}'])]);
Pdf.FileName := TPath.Combine(OutputDir, OutName);

Un id de cerere sau un id de sarcină funcționează la fel de bine ca GUID-ul atunci când cadrul din jur vă dă deja unul, iar numele fișierului devine astfel, pe gratis, urmăribil până la o linie de jurnal. Oricum ar fi, principiul este același: proiectați astfel încât fișierul pe care îl scrieți să fie numai al dumneavoastră în momentul în care îl scrieți. Blocarea dispare nu pentru că ați forțat închiderea unei ferestre, ci pentru că nimic altceva nu atinge octeții

Forma corecturii

Reduceți cele două probleme la rădăcinile lor și amândouă țin de respectarea granițelor. Eroarea mașinii de stări vă cere să respectați granița instanței: un THotPDF, un document, apoi lăsați-l să plece și faceți altul. Eroarea de blocare a fișierului vă cere să respectați granița fișierului: scrieți acolo unde nimic altceva nu citește, apoi mutați rezultatul la locul lui. Niciuna nu cere peticirea bibliotecii sau scriptarea desktopului. Amândouă decurg din tratarea fiecărui document ca pe o unitate de lucru de sine stătătoare, creată de la zero, scrisă curat și eliberată, adică același tipar care face previzibil restul componentei

Apelurile BeginDoc, EndDoc, LoadFromFile și SaveLoadedDocument prezentate aici fac parte din HotPDF Delphi Component pentru Delphi și C++Builder