Techninis straipsnis

THotPDF egzemplioriaus pakartotinis naudojimas keliems dokumentams Delphi aplinkoje

Klaidos pranešimas teigia Please load the document before using BeginDoc, ir jis beveik visada pasirodo antrą kartą. Pirmasis dokumentas įrašomas puikiai. Tada to paties THotPDF egzemplioriaus (angl. instance) prašoma pradėti antrąjį, BeginDoc išmeta išimtį, o pranešimas nurodo į dokumento įkėlimą, o tai yra priešinga tam, ką kodas bando padaryti. Simptomo ir pranešimo neatitikimas yra tai, dėl ko ši klaida įsimena. Tikroji tema yra komponento gyvavimo ciklas, ir kai tai tampa aišku, klaida nustoja būti paslaptinga

THotPDF document lifecycle showing Create, BeginDoc, EndDoc, and Free per output file
Vienas THotPDF egzempliorius atitinka vieną dokumentą: Create, BeginDoc, piešimas (angl. draw), EndDoc, Free.

Pakartotinis komponento naudojimas reikalauja atskirti dokumento būseną nuo puslapio būsenos. Prieš atidarydami kitą failą užbaikite ankstesnį dokumentą, atlaisvinkite laikinuosius objektus ir atstatykite nustatymus, kurie nėra automatiškai grąžinami į pradines reikšmes

Naujas egzempliorius kiekvienam įvesties failui yra paprasčiausia taisyklė, kai svarbus patikimumas. Ji pašalina paslėptą ankstesnio dokumento būseną ir leidžia aiškiai susieti išteklių gyvavimo ciklą su vienu išvesties failu

Jei egzempliorių būtina naudoti pakartotinai, valymo veiksmus atlikite prieš naują aktyvavimą ir po jo patikrinkite, kad dokumentas nebeturi atvirų failo rankenų, puslapių ar laikinų šriftų objektų

Šis modelis taip pat supaprastina lygiagretų apdorojimą: kiekviena gija turi savo komponento būseną, todėl vienas dokumentas negali netyčia perrašyti kito dokumento nustatymų

„THotPDF“ egzempliorius yra vienas dokumentas, o ne dokumentų gamykla

Gundantis psichologinis modelis yra tas, kad THotPDF yra paslaugos objektas (angl. service object), kurį paleidžiate vieną kartą ir maitinate dokumentais, taip, kaip galėtumėte laikyti atidarytą duomenų bazės ryšį ir vykdyti užklausą po užklausos per jį. Taip nėra. Egzempliorius modeliuoja vieną kuriamą dokumentą, o jo vidinė būsenų mašina (angl. state machine) daro prielaidą, kad ji pereina kelią vieną kartą: nuo tuščio, per atidarytą dokumentą, iki išsaugoto failo. BeginDoc atidaro tą kelią ir pažymi egzempliorių kaip turintį vykdomą dokumentą. EndDoc serijuoja viską į FileName ir jį uždaro. Vėl iškvietus BeginDoc tame pačiame užbaigtame egzemplioriuje prašoma jo iš naujo įeiti į būseną, kurios jis niekada švariai nepaliko, ir suveikia apsauga, kurios pranešimas atsitiktinai užsimena apie įkėlimą, nes viduje sąlygos „pasiruošęs pradėti“ ir „turi įkeltą dokumentą“ yra tikrinamos kartu

Taigi pranešimas yra klaidinantis, bet apsauga atlieka savo darbą. Ji atsisako leisti jums pradėti naują dokumentą ant komponento, kuris vis dar tiki, kad yra dokumento viduryje. Sprendimas nėra nugalėti apsaugą. Tai yra nustoti pakartotinai naudoti išeikvotą egzempliorių

Gyvavimo ciklas, tokia tvarka, kokia jis turi įvykti

Kiekvienas dokumentas, kurį „HotPDF“ rašo nuo nulio, seka tuos pačius keturis taktus, ir dėl tvarkos nesiderama. Create paskirsto komponentą. BeginDoc atidaro dokumentą ir užfiksuoja struktūrinius pasirinkimus, todėl viskas, kas turi įtakos visam failui (puslapio dydis, glaudinimas, šifravimas, išvesties failo pavadinimas), turi būti nustatyta tarp Create ir BeginDoc. Tada jūs piešiate. Tada EndDoc įrašo baitus į diską. Free atlaisvina egzempliorių. Piešimo iškvietimai, pateikti prieš BeginDoc, neturi puslapio, kuriame galėtų nusileisti; viso dokumento savybės, priskirtos po jo, yra ignoruojamos be jokių skundų

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;

Skaitykite tai kaip darbo vienetą. Vienas Create, vienas BeginDoc, vienas EndDoc, vienas Free, vienas failas diske. Tą akimirką, kai norite antro failo, pradedate naują darbo vienetą, o tai reiškia naują egzempliorių

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

Lūžtanti versija bando būti taupi paskirstydama: sukonstruokite komponentą vieną kartą, pereikite per partiją (angl. batch), iškvieskite BeginDoc ir EndDoc ciklo viduje. Antroji iteracija išmeta išimtį. Veikianti versija traktuoja kiekvieną išvestį kaip savo trumpalaikį objektą, o komponento sukūrimo paskirstymo kaina yra nereikšminga lyginant su PDF išdėstymo ir serijavimo darbu, todėl nėra ko sutaupyti kaupiant egzemplierių

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;

try/finally blokas, esantis ciklo viduje, yra ta dalis, kurią verta ginti peržiūroje. Jei BeginDoc arba bet koks piešimo iškvietimas išmeta išimtį įpusėjus vienam dokumentui, tos iteracijos egzempliorius vis tiek atlaisvinamas prieš prasidedant kitai, todėl vienas blogas įrašas nepalieka pusiau sukonstruoto komponento ir neapnuodija likusio vykdymo. Ištraukite Create virš ciklo, kad „optimizuotumėte“, ir jūs grįšite prie pradinės klaidos, dabar dėvinčios partijos ciklo drabužius

Esamo failo modifikavimas yra kitas įėjimo taškas

Yra antrasis „pakartotinio naudojimo“ skaitymas, kuris yra visiškai teisėtas: nenorite tuščio dokumento, norite atidaryti jau esantį PDF ir jį pakeisti. Tas kelias visiškai neina per BeginDoc, ir būtent todėl klaidos pranešime įvardijamas įkėlimas. Jūs įkeliate failą, redaguojate jį ir išsaugote bet kokiu 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ė ar mažesnė reikšmė reiškia, kad įkėlimas nepavyko, todėl verta patikrinti prieš paliečiant CurrentPage. Poravimas yra svarbus: dokumentas, kurį atidarėte naudodami LoadFromFile, išsaugomas naudojant SaveLoadedDocument, o ne su BeginDoc/EndDoc pora, kuri priklauso dokumentams, kuriuos kuriate nuo nulio. Šių dviejų sumaišymas yra labiausiai paplitęs būdas supainioti tą pačią būsenų mašiną, kuri sukėlė pradinę klaidą. Laikykite šiuos du srautus mintyse atskirus: BeginDoc ... EndDoc kuria, LoadFromFile ... SaveLoadedDocument redaguoja

Failo užrakto problema yra tikra, ir atsakymas nėra žudyti peržiūros programos langus

Pakartotinio naudojimo klaida dažnai keliauja su antruoju skundu, ir jie abu susipainioja, nes iškyla toje pačioje failo regeneravimo darbo eigoje. Naudotojas atidaro PDF failą, kurį ką tik sukūrėte, palieka jį atidarytą „Acrobat“ ar „Foxit“ programoje, tada suaktyvina perkonstravimą (angl. rebuild). EndDoc bando rašyti į tą patį kelią, operacinė sistema atsisako, nes peržiūros programa laiko skaitymo dalį, kuri blokuoja rašytojus, ir jūs gaunate prieigos atsisakymo gedimą. Tai yra tikrai „Windows“ failų užrakinimo (angl. file-locking) problema, o ne komponento būsenos problema, ir ji nusipelno tikro atsakymo, o ne problemos apėjimo (angl. workaround)

Problemos apėjimas, kuris cirkuliuoja, išvardijant aukščiausio lygio langus ir siunčiant WM_CLOSE bet kam, kieno pavadinimas atrodo kaip PDF peržiūros programa, yra klaidingas instinktas. Jis peržengia procesų ribas, kad uždarytų langus, kurie nepriklauso jūsų programai, atspėja peržiūros programas pagal pavadinimo tekstą ir gali išmesti naudotojo neišsaugotas anotacijas neatsiklausęs. Elkitės su visu šiuo požiūriu kaip su blogo kodo kvapu (angl. code smell). Patikimas sprendimas yra niekada nerašyti į kelią, kurį gali laikyti kitas procesas. Serijuokite į laikiną failą tame pačiame kataloge, tada sukeiskite jį vietoje naudodami atominį pervardijimą (angl. atomic rename), kai EndDoc pavyksta. Jei peržiūros programoje vis dar atidarytas senasis failas, pervardijimas arba pavyksta švariai, arba garsiai nepavyksta, ir jūs pateikiate aiškų pranešimą, o ne kovojate su užraktu

Didelės apimties serveriui, kuris nuolat regeneruoja dokumentus, švaresnė disciplina yra rašyti kiekvieną išvestį unikaliu pavadinimu (laiko žyma (angl. timestamp) arba užduoties ID), kad du paleidimai niekada nesivaržytų dėl vieno kelio, ir leisti atskirai saugojimo politikai išvalyti senus failus. Bet kokiu atveju principas yra tas pats: projektuokite taip, kad failas, kurį rašote, rašymo momentu priklausytų tik jums. Užraktas dingsta ne todėl, kad per prievartą uždarėte langą, o todėl, kad niekas kitas neliečia baitų

Sprendimo forma

Išnarpliokite šias dvi problemas iki jų šaknų ir pamatysite, kad abi jos yra apie ribų gerbimą. Būsenų mašinos klaida nori, kad gerbtumėte egzemplioriaus ribą: vienas THotPDF, vienas dokumentas, tada paleiskite jį ir sukurkite kitą. Failo užrakto klaida nori, kad gerbtumėte failo ribą: rašykite ten, kur niekas kitas neskaito, tada perkelkite rezultatą į vietą. Nė vienai iš jų nereikia pataisyti bibliotekos ar rašyti skriptų darbalaukiui. Abi jos išsprendžiamos traktuojant kiekvieną dokumentą kaip savarankišką darbo vienetą, sukurtą iš naujo, parašytą švariai ir atlaisvintą, o tai yra tas pats modelis, kuris daro likusią komponento dalį nuspėjamą

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