Tehnički članak

Ponovna upotreba instance THotPDF kroz dokumente u Delphi-ju

Greška glasi Please load the document before using BeginDoc i gotovo uvek se pojavljuje pri drugom prolazu. Prvi dokument se upiše bez problema. Zatim ista instanca THotPDF pokuša da započne drugi dokument, BeginDoc izazove izuzetak, a poruka govori o učitavanju dokumenta, što je suprotno nameri koda. Nesklad simptoma i poruke čini problem zbunjujućim. Stvarni uzrok je životni ciklus komponente; kada ga razumete, greška prestaje da bude tajanstvena

Jedna instanca THotPDF predstavlja jedan dokument

Primamljiv mentalni model je da je THotPDF servisni objekat koji jednom pokrenete i zatim mu redom prosleđujete dokumente, kao dugotrajnu vezu sa bazom podataka. Nije tako. Instanca predstavlja jedan dokument u izgradnji, a njena interna mašina stanja pretpostavlja samo jedan prolaz: od praznog stanja, preko otvorenog dokumenta, do sačuvane datoteke. BeginDoc otvara taj prolaz i označava dokument kao aktivan. EndDoc serijalizuje sadržaj u FileName i završava ga. Ponovni BeginDoc na već završenoj instanci pokušava da je vrati u stanje koje nije napustila na način predviđen za ponovnu upotrebu; aktivira se zaštita čija poruka pominje učitavanje jer se uslovi „spreman za početak“ i „ima učitan dokument“ interno proveravaju zajedno

Poruka je zavaravajuća, ali zaštita radi svoj posao: sprečava novi dokument preko komponente koja i dalje nosi stanje prethodnog. Rešenje nije zaobići proveru, već prestati sa ponovnom upotrebom potrošene instance

Životni ciklus u obaveznom redosledu

Svaki dokument koji HotPDF piše od nule prolazi kroz ista četiri koraka. Create alocira komponentu. BeginDoc otvara dokument i zaključava strukturne odluke, pa se svojstva cele datoteke, kao što su veličina stranice, kompresija, šifrovanje i izlazno ime, postavljaju između Create i BeginDoc. Zatim crtate. EndDoc upisuje bajtove na disk, a Free oslobađa instancu. Pozivi za crtanje pre BeginDoc nemaju stranicu na koju bi pisali, dok se svojstva dokumenta postavljena posle njega tiho zanemaruju

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;

To je jedinica rada: jedan Create, jedan BeginDoc, jedan EndDoc, jedan Free i jedna datoteka na disku. Čim želite drugu datoteku, započinjete novu jedinicu rada i potrebna vam je nova instanca

Ispravna ponovna upotreba znači nova instanca po datoteci

Neispravna verzija pokušava da uštedi alokaciju: komponentu kreira jednom, prolazi kroz grupu i poziva BeginDoc i EndDoc unutar petlje. Druga iteracija pada. Ispravna verzija tretira svaki izlaz kao zaseban kratkotrajan objekat. Trošak kreiranja komponente zanemarljiv je u odnosu na raspored i serijalizaciju PDF-a, pa čuvanje instance ne donosi stvarnu uštedu

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;

Blok try/finally unutar petlje treba sačuvati. Ako BeginDoc ili crtanje izazovu izuzetak usred jednog dokumenta, instanca te iteracije ipak se oslobađa pre početka sledeće, pa jedan loš zapis ne ostavlja napola izgrađenu komponentu koja kvari ostatak grupe. Izmeštanje Create iznad petlje radi „optimizacije“ vraća prvobitnu grešku u novom obliku

Izmena postojeće datoteke koristi drugi ulaz

Drugo značenje „ponovne upotrebe“ potpuno je opravdano: ne želite prazan dokument, već da otvorite postojeći PDF i izmenite ga. Ta putanja uopšte ne koristi BeginDoc, što objašnjava zašto poruka greške pominje učitavanje. Učitajte datoteku, izmenite je i sačuvajte pod željenim imenom

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 vraća broj stranica, a nula ili negativna vrednost znači da učitavanje nije uspelo, pa rezultat proverite pre pristupa svojstvu CurrentPage. Uparivanje je važno: dokument otvoren pomoću LoadFromFile čuva se pozivom SaveLoadedDocument, a ne parom BeginDoc/EndDoc, koji pripada dokumentima napravljenim od nule. Mešanje tih tokova najčešći je način da se zbuni ista mašina stanja koja je proizvela prvobitnu poruku. Zapamtite: BeginDocEndDoc stvara, a LoadFromFileSaveLoadedDocument uređuje

Zaključavanje datoteke je stvarno, ali ne gasite čitače

Greška ponovne upotrebe često dolazi sa drugim problemom jer oba izlaze na videlo pri regenerisanju iste datoteke. Korisnik otvori PDF u Acrobat-u ili Foxit-u, ostavi ga otvorenog i pokrene ponovnu izgradnju. EndDoc pokušava da upiše istu putanju, a Windows odbija pristup zato što čitač drži deljenje koje blokira pisanje. To je pravi problem zaključavanja datoteke, odvojen od stanja komponente, i zahteva odgovarajuće rešenje

Zaobilazno rešenje koje enumeriše prozore najvišeg nivoa i šalje WM_CLOSE svemu čiji naslov liči na PDF čitač pogrešan je pristup. Zatvara prozore drugog procesa, pogađa program prema tekstu naslova i može odbaciti nesačuvane anotacije korisnika. Pouzdano rešenje je da nikada ne pišete direktno na putanju koju drugi proces možda drži. Serijalizujte u privremenu datoteku u istom direktorijumu, pa je nakon uspešnog EndDoc zamenite atomskim preimenovanjem. Ako čitač i dalje drži staru datoteku, zamena uspeva čisto ili jasno pada, a aplikacija prikazuje razumljivu poruku umesto da se bori sa zaključavanjem

uses
  System.SysUtils, System.IOUtils;

procedure WritePdfAtomically(const FinalPath: string);
var
  Pdf: THotPDF;
  TempPath: string;
begin
  // Temp file in the SAME directory as the target: a rename inside one
  // NTFS volume swaps the name atomically, while a cross-volume move
  // degrades to copy-plus-delete and loses that guarantee
  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;                    // the temp file is complete on disk here
    finally
      Pdf.Free;
    end;

    // Swap into place. TFile.Move refuses to overwrite, so clear a stale
    // target first; if a viewer still holds the old file, the delete is
    // what fails, loudly, before the good bytes are touched
    if TFile.Exists(FinalPath) then
      TFile.Delete(FinalPath);
    TFile.Move(TempPath, FinalPath); // or: RenameFile(TempPath, FinalPath)
  except
    if TFile.Exists(TempPath) then
      TFile.Delete(TempPath);        // never strand a half-written temp file
    raise;
  end;
end;

Dve napomene su važne. TFile.Move i klasični RenameFile mapiraju se na Windows preimenovanje, koje je atomsko samo kada su izvor i odredište na istom volumenu; zato privremena datoteka pripada odredišnom direktorijumu, a ne TPath.GetTempPath. Takođe, par brisanje-pa-pomeranje nije jedan atomski korak: kratko vreme nijedna datoteka ne postoji. Za desktop aplikaciju koja regeneriše izveštaj to obično nije bitno. Ako je potreban jači ugovor na istom volumenu, pozovite Win32 ReplaceFile ili MoveFileEx sa MOVEFILE_REPLACE_EXISTING, što zamenu svodi na jedan poziv

Za server velikog obima koji stalno regeneriše dokumente, čistije pravilo je da svaki izlaz dobije jedinstveno ime, vremensku oznaku ili identifikator posla, tako da se dva pokretanja nikada ne nadmeću za istu putanju. Zasebna politika zadržavanja zatim uklanja stare datoteke

// One output path per request: two concurrent jobs can never contend
// for the same name, so no rename dance and no lock to lose
OutName := Format('statement-%s-%s.pdf',
  [CustomerId, TGUID.NewGuid.ToString.Trim(['{', '}'])]);
Pdf.FileName := TPath.Combine(OutputDir, OutName);

Identifikator zahteva ili posla jednako je dobar kao GUID kada ga okruženje već obezbeđuje, a ime datoteke tada se besplatno povezuje sa odgovarajućim zapisom dnevnika. Načelo je isto: datoteka koju upravo pišete mora u tom trenutku pripadati samo vašem procesu. Zaključavanje nestaje zato što ništa drugo ne dodiruje iste bajtove, a ne zato što ste prisilno zatvorili tuđ prozor

Suština ispravke

Oba problema svode se na poštovanje granica. Greška mašine stanja zahteva granicu instance: jedan THotPDF, jedan dokument, zatim oslobodite instancu i napravite novu. Greška zaključavanja zahteva granicu datoteke: pišite tamo gde niko ne čita, pa premestite rezultat na odredište. Nijedno rešenje ne zahteva zakrpu biblioteke niti automatizovanje radne površine. Oba proizlaze iz tretiranja svakog dokumenta kao samostalne jedinice rada koja se sveže kreira, čisto upisuje i zatim oslobađa

Pozivi BeginDoc, EndDoc, LoadFromFile i SaveLoadedDocument prikazani ovde deo su HotPDF Component za Delphi i C++Builder