Műszaki cikk

THotPDF példány újrahasználata dokumentumok között

A hibaüzenet így szól: Please load the document before using BeginDoc, és szinte mindig másodszorra bukkan fel. Az első dokumentum rendben megíródik. Aztán ugyanazt a THotPDF példányt megkérik, hogy kezdjen egy másodikat, a BeginDoc kivételt dob, az üzenet pedig egy dokumentum betöltésére mutat, ami épp az ellenkezője annak, amit a kód tenni próbál. A tünet és az üzenet közti eltérés az, amitől ez a hiba megragad. A valódi téma a komponens életciklusa, és amint ez a helyére kattan, a hiba megszűnik rejtélyes lenni

A THotPDF dokumentum-életciklusa, amely kimeneti fájlonként a Create, BeginDoc, EndDoc és Free lépéseket mutatja
Egy THotPDF példány egy dokumentumnak felel meg: Create, BeginDoc, rajzolás, EndDoc, Free

A THotPDF példány egy dokumentum, nem dokumentumgyár

A csábító gondolati modell az, hogy a THotPDF szolgáltatásobjektum, amelyet egyszer indít el, majd dokumentumokkal etet, ahogy egy adatbázis-kapcsolatot tartana nyitva, és lekérdezést lekérdezés után futtatna rajta. Nem ez a helyzet. Egy példány egyetlen épülő dokumentumot modellez, és a belső állapotgépe azt a feltevést hordozza, hogy egyszer járja végig az utat: az üres állapotból egy nyitott dokumentumon át egy mentett fájlig. A BeginDoc nyitja meg ezt az utat, és megjelöli a példányt úgy, hogy folyamatban lévő dokumentuma van. Az EndDoc mindent a FileName útvonalra sorosít és lezárja. Ha ugyanazon a befejezett példányon újra hívja a BeginDoc metódust, azt kéri tőle, hogy lépjen vissza olyan állapotba, amelyet soha nem hagyott el tisztán, és az az őr sül el, amelynek üzenete véletlenül a betöltést említi, mert belül a „kezdésre kész” és a „van betöltött dokumentuma” feltételt együtt ellenőrzi

Az üzenet tehát félrevezető, de az őr a dolgát végzi. Megtagadja, hogy friss dokumentumot kezdjen egy olyan komponens tetején, amely még mindig azt hiszi, dokumentum közepén jár. A javítás nem az őr kicselezése. Hanem az, hogy hagyja abba az elhasznált példány újrahasználatát

Az életciklus abban a sorrendben, ahogy történnie kell

Minden dokumentum, amelyet a HotPDF a nulláról ír, ugyanazt a négy ütemet követi, és a sorrend nem tárgyalható. A Create lefoglalja a komponenst. A BeginDoc megnyitja a dokumentumot és rögzíti a szerkezeti döntéseket, tehát mindent, ami a teljes fájlra hat (oldalméret, tömörítés, titkosítás, kimeneti fájlnév), a Create és a BeginDoc között kell beállítani. Utána rajzol. Aztán az EndDoc lemezre írja a bájtokat. A Free elengedi a példányt. Azoknak a rajzolási hívásoknak, amelyek a BeginDoc elé kerülnek, nincs oldaluk, ahová leszállhatnának; a dokumentumszintű tulajdonságokat pedig, ha utána adja meg, panasz nélkül figyelmen kívül hagyja

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'invoice.pdf';
    Pdf.BeginDoc;                        // megnyitja a dokumentumot
    Pdf.CurrentPage.SetFont('Arial', [], 11);
    Pdf.CurrentPage.TextOut(50, 760, 0, 'Invoice 2026-042');
    Pdf.EndDoc;                          // kiírja az invoice.pdf fájlt és lezárja
  finally
    Pdf.Free;                            // egy példány, egy dokumentum
  end;
end;

Olvassa ezt munkaegységként. Egy Create, egy BeginDoc, egy EndDoc, egy Free, egy fájl a lemezen. Abban a pillanatban, amikor második fájlt akar, új munkaegységet kezd, ami új példányt jelent

Mit jelentsen az „újrahasználat”: fájlonként friss példányt

A törött változat takarékos akar lenni a foglalással: egyszer építi fel a komponenst, végigmegy egy kötegen, és a cikluson belül hívja a BeginDoc és EndDoc metódust. A második iteráció kivételt dob. A működő változat minden kimenetet saját, rövid életű objektumként kezel, és egy komponens létrehozásának foglalási költsége elenyésző egy PDF elrendezésének és sorosításának munkájához képest, tehát nincs mit megtakarítani a példány felhalmozásával

procedure WriteBatch(const Names: TArray<string>);
var
  I: Integer;
  Pdf: THotPDF;
begin
  for I := 0 to High(Names) do
  begin
    Pdf := THotPDF.Create(nil);         // minden körben új példány
    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;

A cikluson belül ülő try/finally az a rész, amelyet érdemes megvédeni egy kódátnézésen. Ha a BeginDoc vagy bármelyik rajzolási hívás félúton kivételt dob egy dokumentumon belül, az adott iteráció példánya akkor is felszabadul, mielőtt a következő elkezdődne, tehát egyetlen rossz rekord nem hagy magára egy félig felépített komponenst, és nem mérgezi meg a futás többi részét. Emelje ki a Create hívást a ciklus fölé „optimalizálásként”, és máris az eredeti hibánál tart, csak most kötegelt ciklusba öltözve

Egy meglévő fájl módosítása másik belépési pont

Az „újrahasználat” szónak van egy teljesen jogos második olvasata: nem üres dokumentumot akar, hanem egy már létező PDF-et megnyitni és megváltoztatni. Ez az út egyáltalán nem megy át a BeginDoc metóduson, és pontosan ezért említ betöltést a hibaüzenet. Betölti a fájlt, szerkeszti, és tetszőleges néven menti

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;

A LoadFromFile az oldalszámot adja vissza, és a nulla vagy annál kisebb érték sikertelen betöltést jelent, tehát érdemes ellenőrizni, mielőtt a CurrentPage tulajdonsághoz nyúlna. A párosítás számít: azt a dokumentumot, amelyet a LoadFromFile hívással nyitott meg, a SaveLoadedDocument metódussal kell menteni, nem a BeginDoc/EndDoc párossal, amely a nulláról írt dokumentumoké. A kettő keverése a leggyakoribb mód arra, hogy összezavarja ugyanazt az állapotgépet, amely az eredeti hibát is előállította. Tartsa a két folyamot gondolatban külön: a BeginDoc ... EndDoc létrehoz, a LoadFromFile ... SaveLoadedDocument szerkeszt

A fájlzárolás gondja valós, és a megoldás nem a megjelenítőablakok kilövése

Az újrahasználati hiba gyakran egy második panasszal együtt utazik, és a kettő azért gabalyodik össze, mert ugyanabban az újragenerálási munkafolyamatban bukkannak fel. Egy felhasználó megnyitja az imént előállított PDF-et, nyitva hagyja Acrobatban vagy Foxitban, majd újraépítést indít. Az EndDoc ugyanarra az útvonalra próbál írni, az operációs rendszer megtagadja, mert a megjelenítő olyan olvasási megosztást tart, amely kizárja az írókat, és hozzáférés-megtagadási hibát kap. Ez valóban Windows-fájlzárolási kérdés, nem komponensállapot-kérdés, és valódi választ érdemel, nem kerülő megoldást

Az a kerülő megoldás, amely kering — hogy felsorolja a legfelső szintű ablakokat, és WM_CLOSE üzenetet küld mindennek, aminek a címe PDF-megjelenítőre hasonlít —, rossz ösztön. Folyamathatárokon nyúl át, hogy olyan ablakokat zárjon be, amelyeket a programja nem birtokol, a címszöveg alapján találgatja a megjelenítőket, és kérdés nélkül eldobhatja a felhasználó nem mentett jegyzeteit. Kezelje az egész megközelítést rossz jelként. A megbízható javítás az, hogy soha ne írjon olyan útvonalra, amelyet más folyamat tarthat. Sorosítson ideiglenes fájlba ugyanabban a könyvtárban, majd cserélje a helyére atomi átnevezéssel, ha az EndDoc sikerrel járt. Ha egy megjelenítőnél még mindig nyitva van a régi fájl, az átnevezés vagy tisztán sikerül, vagy hangosan elhasal, és Ön világos üzenetet jelenít meg ahelyett, hogy a zárral küzdene

uses
  System.SysUtils, System.IOUtils;

procedure WritePdfAtomically(const FinalPath: string);
var
  Pdf: THotPDF;
  TempPath: string;
begin
  // Ideiglenes fájl a célfájllal AZONOS könyvtárban: az átnevezés egyetlen
  // NTFS köteten belül atomi módon cseréli a nevet, míg a kötetek közti
  // mozgatás másolásra és törlésre esik szét, és elveszti ezt a garanciát
  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;                    // az ideiglenes fájl itt már teljes a lemezen
    finally
      Pdf.Free;
    end;

    // Csere a helyére. A TFile.Move nem ír felül, ezért előbb takarítsa el az
    // elavult célfájlt; ha egy megjelenítő még fogja a régi fájlt, a törlés az,
    // ami hangosan elhasal, mielőtt a jó bájtokhoz bárki hozzáérne
    if TFile.Exists(FinalPath) then
      TFile.Delete(FinalPath);
    TFile.Move(TempPath, FinalPath); // vagy: RenameFile(TempPath, FinalPath)
  except
    if TFile.Exists(TempPath) then
      TFile.Delete(TempPath);        // sose hagyjon magára félig írt ideiglenes fájlt
    raise;
  end;
end;

Két őszinte lábjegyzet ehhez a kódhoz. A TFile.Move és a klasszikus RenameFile ugyanarra a Windows-átnevezésre képződik le, amely csak akkor atomi, ha a forrás és a cél ugyanazon a köteten van, és pontosan ezért kerül az ideiglenes fájl a célkönyvtárba, nem a TPath.GetTempPath helyére. A törlés és a mozgatás párosa pedig önmagában nem egyetlen atomi lépés: van egy rövid ablak, amelyben egyik fájl sem létezik. Egy jelentést újragyártó asztali alkalmazásnál ez az ablak lényegtelen; azok az olvasók, akiknek ugyanazon a köteten erősebb szerződés kell, közvetlenül hívhatják a Win32 ReplaceFile függvényt vagy a MoveFileEx hívást a MOVEFILE_REPLACE_EXISTING jelzővel, ami egyetlen hívássá vonja össze a cserét

Egy nagy forgalmú kiszolgálón, amely folyamatosan újragenerálja a dokumentumokat, a tisztább fegyelem az, hogy minden kimenetet egyedi néven ír ki (időbélyeggel vagy feladatazonosítóval), így két futás soha nem verseng egyetlen útvonalért, a régi fájlokat pedig külön megőrzési irányelv takarítja. A minta kérésenként egyetlen sornyi elnevezési fegyelem

// Kérésenként egy kimeneti útvonal: két párhuzamos feladat soha nem versenghet
// ugyanazért a névért, tehát nincs átnevezési tánc és nincs elvesztendő zár
OutName := Format('statement-%s-%s.pdf',
  [CustomerId, TGUID.NewGuid.ToString.Trim(['{', '}'])]);
Pdf.FileName := TPath.Combine(OutputDir, OutName);

Egy kérés- vagy feladatazonosító ugyanolyan jól működik, mint a GUID, ha a környező keretrendszer amúgy is ad egyet, ráadásul ingyen visszakövethetővé teszi a fájlnevet egy naplósorig. Az elv mindkét esetben ugyanaz: úgy tervezzen, hogy az a fájl, amelyet ír, kizárólag az Öné legyen abban a pillanatban, amikor írja. A zár nem azért tűnik el, mert erőszakkal becsukott egy ablakot, hanem mert semmi más nem nyúl a bájtokhoz

A javítás alakja

Fejtse vissza a két gondot a gyökeréig, és mindkettő a határok tiszteletben tartásáról szól. Az állapotgép hibája azt kéri, hogy tartsa tiszteletben a példányhatárt: egy THotPDF, egy dokumentum, aztán engedje el és készítsen újat. A fájlzárolási hiba azt kéri, hogy tartsa tiszteletben a fájlhatárt: oda írjon, ahol semmi más nem olvas, aztán mozgassa a helyére az eredményt. Egyik sem kíván könyvtárfoltozást vagy asztali szkriptelést. Mindkettő abból következik, hogy minden dokumentumot önálló munkaegységként kezel: frissen létrehozva, tisztán kiírva és elengedve — ugyanaz a minta, amely a komponens többi részét is kiszámíthatóvá teszi

Az itt bemutatott BeginDoc, EndDoc, LoadFromFile és SaveLoadedDocument hívások a HotPDF Delphi Component részei, Delphihez és C++Builderhez