Technical Article

Ponovna uporaba THotPDF instance kroz dokumente u Delphiju

Pogreška glasi Please load the document before using BeginDoc i gotovo se uvijek pojavljuje drugi put. Prvi se dokument zapisuje bez problema. Zatim se od iste THotPDF instance traži da započne drugi dokument, poziv BeginDoc javlja pogrešku, a poruka upućuje na učitavanje dokumenta, što je suprotno od onoga što kod pokušava učiniti. Nesklad između simptoma i poruke je ono zbog čega je ovaj problem zbunjujuć. Prava tema je životni ciklus komponente, a jednom kada to shvatite, pogreška prestaje biti tajanstvena.

Životni ciklus THotPDF dokumenta koji prikazuje Create, BeginDoc, EndDoc i Free po izlaznoj datoteci
Jedna THotPDF instanca mapira se na jedan dokument: Create, BeginDoc, crtanje, EndDoc, Free.

THotPDF instanca je jedan dokument, a ne tvornica dokumenata

Privlačan mentalni model je da je THotPDF servisni objekt koji pokrenete jednom i u njega unosite dokumente, na način na koji biste držali otvorenu vezu s bazom podataka i izvršavali upit za upitom. Ali to nije tako. Instanca predstavlja jedan dokument koji se gradi, a njezin unutarnji automat stanja pretpostavlja da prolazi taj put samo jednom: od praznog stanja, preko otvorenog dokumenta, do spremljene datoteke. Poziv BeginDoc otvara taj put i označava instancu kao onu koja ima dokument u tijeku. EndDoc serijalizira sve u FileName i zatvara ga. Ponovno pozivanje BeginDoc na istoj dovršenoj instanci traži od nje ponovni ulazak u stanje koje nikada nije čisto napustila, a zaštita koja se aktivira je ona čija poruka igrom slučaja spominje učitavanje, jer se interno uvjeti "spreman za početak" i "ima učitan dokument" provjeravaju zajedno.

Dakle, poruka zavarava, ali zaštita radi svoj posao. Ona odbija dopustiti da započnete novi dokument na komponenti koja još uvijek vjeruje da je usred dokumenta. Rješenje nije zaobilaženje zaštite, nego prestanak ponovne uporabe iskorištene instance.

Životni ciklus, redoslijedom kojim se mora odvijati

Svaki dokument koji HotPDF piše ispočetka prati ista četiri koraka, a redoslijed nije podložan pregovorima. Poziv Create dodjeljuje memoriju za komponentu. BeginDoc otvara dokument i definira strukturne odluke, pa se sve što utječe na cijelu datoteku (veličina stranice, sažimanje, šifriranje, naziv izlazne datoteke) mora postaviti između Create i BeginDoc. Zatim crtate. Potom EndDoc zapisuje bajtove na disk. Free oslobađa instancu. Pozivi za crtanje postavljeni prije BeginDoc nemaju stranicu na kojoj bi se prikazali, dok se svojstva cijelog dokumenta dodijeljena nakon njega zanemaruju bez ikakvog upozorenja.

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;

Shvatite to kao jedinicu rada. Jedan Create, jedan BeginDoc, one EndDoc, jedan Free, jedna datoteka na disku. U trenutku kada želite drugu datoteku, započinjete novu jedinicu rada, što znači novu instancu.

Što bi "ponovna uporaba" trebala značiti: nova instanca po datoteci

Verzija koja ne radi pokušava biti štedljiva s dodjelom resursa: gradi komponentu jednom, prolazi kroz petlju, poziva BeginDoc i EndDoc unutar petlje. Druga iteracija javlja pogrešku. Verzija koja radi tretira svaki izlaz kao vlastiti kratkotrajni objekt, a trošak dodjele resursa za stvaranje komponente beznačajan je u usporedbi s poslom raspoređivanja i serijalizacije PDF-a, tako da nema uštede u zadržavanju instance.

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 koji se nalazi unutar petlje je dio koda koji vrijedi braniti prilikom pregleda koda. Ako BeginDoc ili bilo koji poziv za crtanje javi pogrešku usred jednog dokumenta, instanca te iteracije i dalje se oslobađa prije nego što počne sljedeća, pa jedan loš zapis ne ostavlja napola izgrađenu komponentu i ne kvari ostatak izvođenja. Ako izvučete Create iznad petlje kako biste "optimizirali" kod, vratit ćete se na izvornu pogrešku, samo što se ovaj put nalazi unutar petlje.

Izmjena postojeće datoteke je drugačija točka ulaza

Postoji i drugo tumačenje "ponovne uporabe" koje je posve legitimno: ne želite prazan dokument, već želite otvoriti PDF koji već postoji i promijeniti ga. Taj put uopće ne prolazi kroz BeginDoc, što je upravo razlog zašto poruka o pogrešci spominje učitavanje. Učitate datoteku, uredite je i spremite pod imenom koje odaberete.

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 vrijednost nula ili manja znači da učitavanje nije uspjelo, pa je to korisno provjeriti prije nego što upotrijebite CurrentPage. Uparivanje je važno: dokument koji ste otvorili pomoću LoadFromFile sprema se pomoću SaveLoadedDocument, a ne pomoću para BeginDoc/EndDoc, koji pripada dokumentima koje stvarate od nule. Miješanje ta dva pristupa najčeći je način da zbunite isti automat stanja koji je proizveo izvornu pogrešku. Držite ta dva toka mentalno odvojenima: BeginDoc ... EndDoc stvara, a LoadFromFile ... SaveLoadedDocument uređuje.

Problem sa zaključavanjem datoteke je stvaran, a rješenje nije gašenje prozora preglednika

Pogreška ponovne uporabe često dolazi s još jednom pritužbom, a te dvije se isprepliću jer se pojavljuju u istom tijeku rada regeneracije datoteke. Korisnik otvori PDF koji ste upravo izradili, ostavi ga otvorenog u Acrobatu ili Foxitu, a zatim pokrene ponovnu izgradnju. EndDoc pokušava pisati na istu putanju, operacijski sustav to odbija jer preglednik drži pravo čitanja koje blokira pisanje, i dobivate pogrešku o uskraćenom pristupu. Ovo je doista problem zaključavanja datoteke u sustavu Windows, a ne problem stanja komponente, i zaslužuje pravo rješenje umjesto privremenog zaobilaženja.

Privremeno rješenje koje kruži internetom, a sastoji se od pretraživanja prozora najviše razine i slanja poruke WM_CLOSE bilo čemu čiji naslov izgleda kao PDF preglednik, krivi je pristup. Ono prelazi granice procesa kako bi zatvorilo prozore koje vaš program ne posjeduje, nagađa preglednike na temelju naslova i može bez pitanja odbaciti nespremljene bilješke korisnika. Smatrajte cijeli taj pristup lošom praksom. Pouzdano rješenje je da nikada ne pišete na putanju koju bi drugi proces mogao držati otvorenim. Serijalizirajte u privremenu datoteku u istom direktoriju, a zatim je zamijenite atomskim preimenovanjem čim EndDoc uspije. Ako preglednik još uvijek ima otvorenu staru datoteku, preimenovanje će ili uspjeti čisto ili javiti jasnu pogrešku, pa ćete moći prikazati razumljivu poruku umjesto borbe sa zaključavanjem.

Za poslužitelj s velikim opterećenjem koji neprestano regenerira dokumente, čišća je praksa pisati svaki izlaz pod jedinstvenim nazivom (vremenska oznaka ili ID zadatka) kako se dva pokretanja nikada ne bi borila za istu putanju, a zasebno pravilo čišćenja neka uklanja stare datoteke. U oba slučaja načelo je isto: dizajnirajte tako da datoteka koju pišete bude isključivo vaša u trenutku pisanja. Zaključavanje nestaje ne zato što ste prisilno zatvorili prozor, već zato što ništa drugo ne dira te bajtove.

Oblik rješenja

Svedite ova dva problema na njihove korijene i vidjet ćete da se oba odnose na poštivanje granica. Pogreška automata stanja traži da poštujete granicu instance: jedan THotPDF, jedan dokument, a zatim ga otpustite i stvorite drugi. Pogreška zaključavanja datoteke traži da poštujete granicu datoteke: pišite tamo gdje ništa drugo ne čita, a zatim premjestite rezultat na njegovo mjesto. Nijedan ne zahtijeva krpanje biblioteke ili skriptiranje radne površine. Oba proizlaze iz tretiranja svakog dokumenta kao samostalne jedinice rada, stvorene iznova, zapisane čisto i oslobođene, što je isti obrazac koji čini ostatak komponente predvidljivim.

Pozivi BeginDoc, EndDoc, LoadFromFile i SaveLoadedDocument prikazani ovdje dio su HotPDF komponente za Delphi i C++Builder.