Technical Article

Opakované použitie inštancie THotPDF pre rôzne dokumenty v Delphi

Chybová správa znie Please load the document before using BeginDoc a takmer vždy sa zobrazí až na druhýkrát. Prvý dokument sa zapíše bez problémov. Potom je však rovnaká inštancia THotPDF požiadaná o spustenie druhého dokumentu, metóda BeginDoc vyvolá výnimku a správa odkazuje na načítanie dokumentu, čo je presný opak toho, o čo sa kód snaží. Tento rozpor medzi príznakom a chybovým hlásením spôsobuje, že problém je mätúci. Skutočným jadrom veci je životný cyklus komponentu. Keď ho pochopíte, táto chyba prestane byť záhadou.

Životný cyklus dokumentu THotPDF zobrazujúci Create, BeginDoc, EndDoc a Free pre každý výstupný súbor
Jedna inštancia THotPDF zodpovedá jednému dokumentu: Create, BeginDoc, kreslenie, EndDoc, Free.

Inštancia THotPDF je jeden dokument, nie továreň na dokumenty

Lákavým mentálnym modelom je, že THotPDF je služobný objekt, ktorý vytvoríte raz a následne mu posielate dokumenty, podobne ako keby ste nechali otvorené pripojenie k databáze a vykonávali jeden dopyt za druhým. Tak to však nefunguje. Inštancia predstavuje jeden vytváraný dokument a jej vnútorný stavový automat predpokladá, že touto cestou prejde iba raz: od prázdneho stavu cez otvorený dokument až po uložený súbor. Metóda BeginDoc otvorí túto cestu a označí inštanciu ako dokument v procese vytvárania. Metóda EndDoc serializuje všetko do súboru FileName a uzavrie ho. Opätovné volanie BeginDoc na tej istej dokončenej inštancii ju núti vrátiť sa do stavu, z ktorého nikdy riadne neodišla, a ochrana, ktorá sa spustí, je tá, ktorej hlásenie náhodou spomína načítanie, pretože interne sa podmienky "pripravený na spustenie" a "má načítaný dokument" kontrolujú spoločne.

Takže správa je síce zavádzajúca, ale ochrana robí svoju prácu. Odmieta vám dovoliť začať nový dokument s komponentom, ktorý si stále myslí, že je uprostred práce na predchádzajúcom dokumente. Riešením nie je obísť túto ochranu, ale prestať opätovne používať už vyčerpanú inštanciu.

Životný cyklus a jeho povinné poradie

Každý dokument, ktorý HotPDF vytvára od nuly, nasleduje rovnaké štyri kroky, pričom ich poradie nie je možné meniť. Create alokuje komponent. BeginDoc otvorí dokument a zafixuje štrukturálne nastavenia, takže všetko, čo ovplyvňuje celý súbor (veľkosť stránky, kompresia, šifrovanie, názov výstupného súboru), sa musí nastaviť medzi Create a BeginDoc. Potom kreslíte. Nakoniec EndDoc zapíše bajty na disk a Free uvoľní inštanciu. Volania kreslenia umiestnené pred BeginDoc nemajú žiadnu stránku, na ktorú by sa dali vykresliť; vlastnosti celého dokumentu priradené po ňom sú ignorované bez akejkoľvek chybovej správy.

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;

Chápte to ako samostatnú pracovnú jednotku. Jedno Create, jedno BeginDoc, jedno EndDoc, jedno Free a jeden súbor na disku. Akonáhle potrebujete druhý súbor, začínate novú pracovnú jednotku, čo znamená aj novú inštanciu.

Čo by malo znamenať "opakované použitie": nová inštancia pre každý súbor

Chybná verzia sa snaží šetriť alokáciou: vytvorí komponent raz, prechádza cyklom a vo vnútri cyklu volá BeginDoc a EndDoc. Druhá iterácia zlyhá na výnimke. Správna verzia pristupuje ku každému výstupu ako k vlastnému krátkodobému objektu. Náklady na alokáciu a vytvorenie komponentu sú zanedbateľné v porovnaní s prácou na rozložení a serializácii PDF, takže neustálym držaním rovnakej inštancie nič neušetríte.

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;

Konštrukcia try/finally vo vnútri cyklu je dôležitá pre stabilitu. Ak BeginDoc alebo akékoľvek kreslenie zlyhá uprostred generovania jedného dokumentu, inštancia danej iterácie sa pred začatím ďalšej riadne uvoľní, takže jedna chybná položka neznefunkční napoly postavený komponent a neovplyvní zvyšok behu. Presunutie Create pred cyklus v rámci "optimalizácie" vás vráti k pôvodnej chybe, tentokrát zabalenej v cykle.

Úprava existujúceho súboru má iný vstupný bod

Existuje aj druhé chápanie "opakovaného použitia", ktoré je úplne legitímne: nechcete prázdny dokument, ale chcete otvoriť už existujúci PDF súbor a upraviť ho. Táto cesta vôbec neprechádza cez BeginDoc, čo je presný dôvod, prečo chybová správa hovorí o načítaní. Súbor načítate, upravíte a uložíte pod ľubovoľným názvom.

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;

Metóda LoadFromFile vracia počet strán a hodnota nula alebo menej znamená, že načítanie zlyhalo, preto sa oplatí túto hodnotu pred prístupom k CurrentPage skontrolovať. Na párovaní záleží: dokument otvorený pomocou LoadFromFile sa ukladá cez SaveLoadedDocument, nie cez pár BeginDoc/EndDoc, ktorý patrí výhradne dokumentom vytváraným od základu. Zmiešanie týchto dvoch prístupov je najčastejším spôsobom, ako zmiasť rovnaký stavový automat, ktorý spôsobil pôvodnú chybu. Udržujte tieto dva toky oddelené: BeginDoc ... EndDoc vytvára, LoadFromFile ... SaveLoadedDocument upravuje.

Problém so zámkom súborov je reálny a riešením nie je zatváranie okien prehliadačov

Chyba pri opakovanom použití inštancie často prichádza s inou komplikáciou, pričom sa tieto dva problémy navzájom pletú, pretože sa objavujú v rovnakom cykle regenerácie súborov. Používateľ otvorí vygenerované PDF, nechá ho otvorené v Acrobat alebo Foxit Readeri a následne spustí opätovné zostavenie. Metóda EndDoc sa pokúsi zapísať na rovnakú cestu, operačný systém to odmietne, pretože prehliadač drží zámok na čítanie, ktorý blokuje zápis, a výsledkom je chyba prístupu (access denied). Toto je skutočný problém so zamykaním súborov vo Windows, nie problém stavu komponentu, a zaslúži si poriadne riešenie namiesto provizórneho obchádzania.

Bežne šírený postup, ktorý spočíva v prechádzaní okien na najvyššej úrovni a posielaní správy WM_CLOSE čomukoľvek, čo v názve vyzerá ako prehliadač PDF, je nesprávny prístup. Zasahuje za hranice vášho procesu a zatvára okná, ktoré váš program nevlastní, odhaduje prehliadače podľa textu v nadpise okna a môže používateľovi bez opýtania zahodiť neuložené poznámky. Považujte takýto prístup za varovný signál. Spoľahlivým riešením je nikdy nezapisovať na cestu, ktorú môže držať iný proces. Serializujte dokument do dočasného súboru v rovnakom adresári a po úspešnom dokončení EndDoc ho premenujte na cieľový súbor pomocou atomickej operácie premenovania. Ak má prehliadač stále otvorený starý súbor, premenovanie buď prebehne úspešne, alebo zlyhá s jasnou chybou, ktorú môžete zobraziť používateľovi, namiesto toho, aby ste bojovali so zámkom.

Pre vysokovýkonný server, ktorý neustále regeneruje dokumenty, je čistejšou disciplínou zapisovať každý výstup pod unikátnym názvom (časová pečiatka alebo ID úlohy), aby sa dva behy nikdy nebili o rovnakú cestu, a nechať čistenie starých súborov na samostatnú politiku uchovávania dát. Princíp je v oboch prípadoch rovnaký: navrhnite systém tak, aby zapisovaný súbor patril v momente zápisu výhradne vám. Zámok nezmizne preto, že ste násilne zatvorili okno, ale preto, že sa s danými bajtmi nepracuje nikde inde.

Podstata opravy

Ak obidva problémy rozmeníme na drobné, zistíme, že v oboch prípadoch ide o rešpektovanie hraníc. Chyba stavového automatu vyžaduje rešpektovanie hraníc inštancie: jeden THotPDF rovná sa jeden dokument, potom inštanciu uvoľniť a vytvoriť novú. Chyba zámku súboru vyžaduje rešpektovanie hraníc súboru: zapisujte tam, kde nikto iný nečíta, a potom výsledok presuňte na miesto. Ani jeden z týchto problémov si nevyžaduje úpravu samotnej knižnice ani skriptovanie operačného systému. Oba sa vyriešia tým, že s každým dokumentom budete zaobchádzať ako so samostatnou jednotkou práce – vytvorenou nanovo, zapísanou čisto a nakoniec uvoľnenou. Presne to je vzor, vďaka ktorému je zvyšok tohto komponentu predvídateľný.

Volania BeginDoc, EndDoc, LoadFromFile a SaveLoadedDocument uvedené v tomto článku sú súčasťou komponentu HotPDF pre Delphi a C++Builder.