Technical Article

THotPDF egzemplioriaus pakartotinis naudojimas keliems dokumentams Delphi

Klaidos pranešimas „Please load the document before using BeginDoc“ beveik visada pasirodo antrąjį kartą. Pirmasis dokumentas įrašomas sėkmingai. Tada to paties THotPDF egzemplioriaus prašoma pradėti antrąjį dokumentą, iškviečiamas BeginDoc ir pranešimas nurodo, kad reikia įkelti dokumentą, nors kodas bando daryti priešingai. Šis neatitikimas tarp simptomo ir pranešimo dažniausiai ir klaidina. Tikroji problema yra susijusi su komponento gyvavimo ciklu, o jį supratus ši klaida nustoja būti paslaptinga.

THotPDF dokumento gyvavimo ciklas, rodantis Create, BeginDoc, EndDoc ir Free vienam išvesties failui
Vienas THotPDF egzempliorius atitinka vieną dokumentą: Create, BeginDoc, piešimas, EndDoc, Free.

THotPDF egzempliorius yra vienas dokumentas, o ne dokumentų gamykla

Gali atrodyti, kad THotPDF yra paslaugų objektas, kurį sukuriate vieną kartą ir vėliau jam perduodate dokumentus, panašiai kaip paliekate atvirą duomenų bazės ryšį ir vykdote vieną užklausą po kitos. Tačiau taip nėra. Egzempliorius modeliuoja vieną kuriamą dokumentą, o jo vidinė būsenų mašina daro prielaidą, kad jis šį kelią praeina tik kartą: nuo tuščio, per atvertą dokumentą, iki išsaugoto failo. BeginDoc atveria šį kelią ir pažymi, kad egzemplioriuje yra kuriamas dokumentas. EndDoc serijuoja viską į FileName ir uždaro procesą. Dar kartą iškvietus BeginDoc tam pačiam užbaigtam egzemplioriui, prašoma sugrįžti į būseną, iš kurios jis niekada švariai neišėjo, o suveikianti apsauga pateikia pranešimą, kuriame minimas įkėlimas, nes viduje sąlygos „pasiruošęs pradėti“ ir „turi įkeltą dokumentą“ yra tikrinamos kartu.

Taigi, pranešimas yra klaidinantis, tačiau apsauga atlieka savo darbą. Ji neleidžia pradėti naujo dokumento komponente, kuris vis dar mano, kad yra dokumento kūrimo viduryje. Sprendimas yra ne apeiti šią apsaugą, o nustoti pakartotinai naudoti jau užbaigtą egzempliorių.

Gyvavimo ciklas ir privaloma jo seka

Kiekvienas dokumentas, kurį HotPDF rašo nuo nulio, seka tuos pačius keturis žingsnius, kurių tvarka negali būti keičiama. Create išskiria atmintį komponentui. BeginDoc atidaro dokumentą ir nustato struktūrinius parametrus, todėl viskas, kas daro įtaką visam failui (puslapio dydis, suspaudimas, šifravimas, išvesties failo pavadinimas), turi būti nustatyta tarp Create ir BeginDoc. Tada atliekamas piešimas. Po to EndDoc įrašo baitus į diską. Galiausiai Free atlaisvina egzempliorių. Piešimo iškvietimai prieš BeginDoc neturi puslapio, kuriame galėtų būti atvaizduoti, o bendros dokumento savybės, priskirtos po jo, yra tiesiog ignoruojamos be jokių įspėjimų.

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;

Laikykite tai vienu darbo vienetu. Vienas Create, vienas BeginDoc, vienas EndDoc, vienas Free, vienas failas diske. Vos tik prireikia antro failo, pradedate naują darbo vienetą, o tai reiškia naują egzempliorių.

Ką turėtų reikšti pakartotinis naudojimas: naujas egzempliorius kiekvienam failui

Klaidinga versija bando taupyti išteklius: sukuria komponentą vieną kartą, sukasi cikle, o jo viduje iškviečia BeginDoc ir EndDoc. Antroji iteracija pateikia klaidą. Teisinga versija kiekvieną išvestį traktuoja kaip atskirą trumpalaikį objektą, o komponento kūrimo sąnaudos yra nereikšmingos, palyginti su PDF puslapio maketavimu ir serijavimu, todėl nėra jokios prasmės taupyti egzemplioriaus sąskaita.

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 ciklo viduje tikrai verta ginti peržiūrint kodą. Jei iškviečiant BeginDoc ar kurį nors piešimo metodą kurio nors dokumento kūrimo metu įvyksta klaida, tos iteracijos egzempliorius vis tiek bus atlaisvintas prieš pradedant kitą. Taip viena sugadinta operacija neužstrigdys pusiau sukurto komponento ir nesugadins likusio ciklo vykdymo. Iškėlę Create virš ciklo norėdami „optimizuoti“, grįšite prie pradinės klaidos, tik dabar jau cikle.

Esamo failo keitimas yra kita įėjimo vieta

Yra antrasis „pakartotinio naudojimo“ supratimas, kuris yra visiškai teisėtas: norite ne tuščio dokumento, o atidaryti jau egzistuojantį PDF ir jį pakeisti. Šis kelias visiškai neina per BeginDoc, todėl klaidos pranešime ir minimas įkėlimas. Jūs įkeliate failą, jį redaguojate ir išsaugote pasirinktu pavadinimu.

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 grąžina puslapių skaičių, o nulinė arba neigiama reikšmė reiškia, kad įkėlimas nepavyko, todėl prieš dirbant su CurrentPage verta tai patikrinti. Porų derinimas yra labai svarbus: dokumentas, kurį atidarėte su LoadFromFile, išsaugomas su SaveLoadedDocument, o ne su BeginDoc/EndDoc pora, kuri skirta kurti dokumentams nuo nulio. Šių dviejų būdų maišymas yra dažniausia priežastis, kodėl sutrinka ta pati būsenų mašina, sukėlusi pradinę klaidą. Laikykite šiuos du srautus atskirai: BeginDoc ... EndDoc kuria naują dokumentą, o LoadFromFile ... SaveLoadedDocument redaguoja esamą.

Failo užrakinimo problema yra reali, ir sprendimas nėra peržiūros programų uždarymas

Pakartotinio naudojimo klaida dažnai pasireiškia kartu su kita problema, kurios susipina, nes atsiranda tame pačiame failo atkūrimo procese. Vartotojas atidaro jūsų sukurtą PDF failą, palieka jį atidarytą „Acrobat“ ar „Foxit“ programoje, o tada paleidžia atkūrimą. EndDoc bando rašyti tuo pačiu keliu, o operacinė sistema atsisako tai daryti, nes peržiūros programa laiko atvirą skaitymo bendrinimą, blokuojantį rašymą, todėl gaunama prieigos klaida. Tai yra grynai „Windows“ failų užrakinimo problema, o ne komponento būsenos problema, todėl jai reikia tikro sprendimo, o ne laikino apėjimo.

Platinamas problemos sprendimo būdas – peržiūrėti visus viršutinio lygio langus ir siųsti pranešimą WM_CLOSE visiems, kurių pavadinimas panašus į PDF peržiūros programą – yra klaidingas instinktas. Taip peržengiamos procesų ribos, bandoma uždaryti langus, kurie nepriklauso jūsų programai, spėliojama apie peržiūros programas pagal lango pavadinimą, be to, galima be įspėjimo išmesti vartotojo neišsaugotus komentarus. Tokį požiūrį reikėtų vertinti kaip blogą kodavimo praktiką. Patikimas sprendimas yra niekada nerašyti tiesiai į kelią, kurį gali laikyti kitas procesas. Serijuokite į laikiną failą tame pačiame kataloge, o tada, kai EndDoc sėkmingai baigia darbą, pakeiskite failus atlikdami atominį pervadinimą. Jei peržiūros programa vis dar yra atidariusi senąjį failą, pervadinimas arba pavyks švariai, arba pateiks aiškią klaidą, todėl galėsite parodyti suprantamą pranešimą vartotojui, užuot kovoję su užraktu.

Didelės apkrovos serveriui, kuris nuolat regeneruoja dokumentus, švaresnė praktika yra įrašyti kiekvieną išvestį unikaliu pavadinimu (laiko žyma arba užduoties ID), kad du paleidimai niekada nekovotų dėl to paties kelio, ir leisti atskirai saugojimo politikai išvalyti senus failus. Bet kuriuo atveju principas išlieka tas pats: projektuokite taip, kad rašomas failas priklausytų tik jums tuo momentu, kai į jį rašote. Užraktas išnyksta ne todėl, kad priverstinai uždarėte langą, o todėl, kad niekas kitas neliečia tų baitų.

Sprendimo esmė

Supaprastinus abi problemas iki jų šaknų, jos abi yra susijusios su ribų gerbimu. Būsenų mašinos klaida reikalauja gerbti egzemplioriaus ribą: vienas THotPDF, vienas dokumentas, tada jį paleisti ir sukurti kitą. Failo užrakinimo klaida reikalauja gerbti failo ribą: rašyti ten, kur niekas kitas neskaito, o tada perkelti rezultatą į reikiamą vietą. Nei vienu atveju nereikia taisyti pačios bibliotekos ar valdyti darbalaukio procesų. Abu sprendimai atsiranda traktuojant kiekvieną dokumentą kaip atskirą darbo vienetą – sukurtą iš naujo, švariai įrašytą ir atlaisvintą – o tai yra tas pats šablonas, kuris daro visas kitas komponento dalis prognozuojamas.

Čia parodyti BeginDoc, EndDoc, LoadFromFile ir SaveLoadedDocument iškvietimai yra HotPDF Component, skirto Delphi ir C++Builder, dalis.