Ko HotPDF Delphi Component naloži datoteko PDF 1.5 s LoadFromFile, ne razčleni objektov, stisnjenih v vsebnike /Type /ObjStm. Zabeleži le, kje živi vsak stisnjeni član, in ga razčleni šele, ko nekaj zanj vpraša. Prav ta lena nespremenljivka drži čas nalaganja sorazmeren s tem, česar se res dotaknete, in je tudi razlog, da mora poln prepis pred izdajo bajtov opraviti eno dodatno delo: razširiti vsakega člana, ki še ni razčlenjen, ker bo prepis pravkar zavrgel vsebnike, v katerih ti člani živijo
Simptom, ki je spodbudil ta zapis, je lahko opisati in neprijetno razhroščevati. Naložite datoteko, katere pisave, barvni prostori in drevo strukture sedijo v tokovih objektov, poženite jo skozi par za izdelavo BeginDoc in EndDoc, in izhod se odpre brez pripomb. Število strani je pravo, besedilo je vidno na straneh, ki jih naključno preverite. Nato sodelavec odpre stran 40 in osnovno besedilo se upodobi v nadomestni pisavi ali pa ukaz Extract Text vrne smeti tam, kjer je bila nekoč zamenjava ActualText. Nič se ni zrušilo. Pisec je preprosto serializiral objekt, ki ni bil nikoli naložen, nenaložen objekt pa se serializira v nič
Kaj LoadFromFile pravzaprav obdrži za stisnjeni objekt?
Za vsak vnos medsebojnih sklicev tipa 2 LoadFromFile obdrži majhen zapis v FCompactObjects: številko objekta, indeks vsebujočega toka v tabeli vsebnikov, položaj člana znotraj tega toka in kazalec ParsedObject, ki se začne kot nil. Vsebnik sam se poišče, če je dokument šifriran, tudi dešifrira, in razširi, telesa članov pa ostanejo kot bajti. ISO 32000-1 §7.5.7 določa postavitev vsebnika, ki to omogoča: glava s pari številke objekta in odmika, nato pa telesa članov, zlepljena za /First, tako da je mogoče vsakega posameznega člana izrezati, ne da bi se dotaknili njegovih sosedov
EnsureCompressedObjectLoaded je edina pot, ki zapis spremeni v objekt. Zapis poišče po številki objekta in če je ParsedObject že nastavljen, vrne ta predpomnjeni objekt ter zabeleži zadetek v predpomnilniku. Sicer vsebnik znova naloži, če je bil izločen, izračuna bajtni obseg člana iz tabele odmikov, razčlenjevalniku preda pogled na ta izrez brez kopiranja in rezultat shrani nazaj v zapis. Od takrat je objekt posreden, nosi svojo resnično številko objekta in je vpisan v kazalo objektov dokumenta kot vsak objekt, razčlenjen iz telesa datoteke. Katalog, slovar z informacijami, koren drevesa strani in objekti strani gredo ob nalaganju po tej poti, ker jih navigacija potrebuje. Pisave, barvni prostori, slovarji ExtGState in elementi strukture ne, in ostanejo kot zapisi, dokler se jih ne dotakne upodabljanje strani ali prepis
To lahko opazujete od zunaj. GetLoadedObjectStreamCacheInfo poroča, koliko vsebnikov obstaja, koliko članov je bilo indeksiranih in koliko od teh je bilo doslej razčlenjenih:
var
Pdf: THotPDF;
Info: THPDFObjectStreamCacheInfo;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.LoadFromFile('tagged-report.pdf');
if Pdf.GetLoadedObjectStreamCacheInfo(Info) then
Writeln(Format('%d containers, %d members indexed, %d parsed so far',
[Info.ContainerCount, Info.IndexedObjectCount,
Info.MaterializedObjectCount]));
finally
Pdf.Free;
end;
end;
Pri datoteki z veliko strukture je tretje število takoj po nalaganju majhen delež drugega. Prav ta vrzel je bistvo lenega nalaganja in je hkrati natanko tisti nabor objektov, po katerega se mora poln prepis vrniti
Zakaj poln prepis izgubi pisave, ki jih inkrementalno shranjevanje obdrži?
Poln prepis zavrže vsebnike /ObjStm in /XRef izvorne datoteke in objektni graf serializira znova od začetka, zato vsak član, katerega ParsedObject je še nil, v izhodu nima več predstavitve. Inkrementalna posodobitev te težave nikoli nima, ker nove objekte priloži za izvirne bajte in stare vsebnike pusti na mestu, da jih naslovi prejšnji odsek medsebojnih sklicev. Razlika ni v tem, kako načina ravnata s pisavami. Razlika je v tem, ali izvirni vsebniki preživijo, da bi jih naslednji pregledovalnik lahko prebral
Popravek živi v SaveToStream, serializatorju, ki ga poganja EndDoc, ne glede na to, ali nastavite FileName ali OutputStream. Preden se razveji v katero koli vejo pisca, prehodi FCompactObjects in na vsakem vnosu pokliče EnsureCompressedObjectLoaded. Če člana ni mogoče naložiti, shranjevanje sproži izjemo, namesto da bi nadaljevalo, ker je prepis, ki tiho izgubi slovar pisave, slabši od tistega, ki se ustavi. Razširitev mora sedeti na tej ravni, nad klasično, pakirano in linearizirano vejo ter nad čiščenjem znova naloženih strukturnih tokov v linearizirani poti. Starejša različica je člane razširila le znotraj SaveLoadedDocument, kar je pokrilo besednjak naloženega dokumenta in povsem zgrešilo besednjak izdelave. LoadFromFile, ki mu sledijo BeginDoc, urejanja strani in EndDoc, je šel naravnost k pisalcu z vsakim nedotaknjenim članom še nerazčlenjenim
// Oba besednjaka prepisa zdaj razširita stisnjene člane, preden steče kateri koli pisec.
// Pot naloženega dokumenta:
Pdf.LoadFromFile('quarterly.pdf');
Pdf.SaveLoadedDocument('quarterly-rewritten.pdf');
// Pot izdelave nad naloženo datoteko:
Pdf.LoadFromFile('quarterly.pdf');
Pdf.FileName := 'quarterly-stamped.pdf';
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 9);
Pdf.CurrentPage.TextOut(40, 20, 0, 'Reviewed 2026-09-11');
Pdf.EndDoc; // SaveToStream najprej materializira vsak vnos v FCompactObjects
Predpomnjeni člani obdržijo vse, kar ste z njimi storili. Objekt, ki je bil pred shranjevanjem razčlenjen, urejen in označen kot umazan, se iz predpomnilnika vrne s svojimi spremembami, član, ki ste ga izbrisali, pa svoje stanje izbrisa obdrži skozi ponavljajoča se shranjevanja. Prehod razširitve je po zasnovi idempotenten: vedno zapolni le mesta z nil
Zakaj preverjanje pik na treh straneh zgreši primer ActualText
Elementi strukture so tisto mesto, kjer se ta napaka skriva najdlje. Vnos ActualText na zaporedju označene vsebine, opredeljen v ISO 32000-1 §14.9.4, nadomesti glife za izvleček in dostopnost, na upodabljanje pa ne vpliva. Če element strukture živi v toku objektov in ga prepis izgubi, se stran še vedno riše pravilno, prva, srednja in zadnja stran se s primerjavo pik ujemajo z izvorom, regresija pa se pokaže šele, ko nekdo požene izvleček besedila ali bralnik zaslona. Test prepisa, ki le upodablja strani, ni test prepisa za označen PDF. Primerjajte tudi izvlečeno besedilo in drevo strukture
Kako prazno uporabniško geslo spremeni nalaganje?
Prazno uporabniško geslo še vedno pomeni, da je datoteka šifrirana, tokovi objektov v taki datoteki pa so šifrirano besedilo, dokler ni ključ datoteke obnovljen. ISO 32000-1 §7.6.3.4, algoritem 2, ta ključ izpelje iz gesla, vnosa /O, /P in prvega identifikatorja dokumenta, HotPDF pa ga mora pognati proti praznemu nizu, preden lahko prehod tipa 2 razširi en sam vsebnik. Zato BeginDoc na naloženem šifriranem dokumentu pred vsem drugim pokliče DecryptLoadedDocument s praznim geslom: objektni graf mora biti overjen in dešifriran, preden se prepis lahko začne, ne glede na to, ali klicatelj namerava izhod zaščititi. Šifriranje izhoda je ločena odločitev, ki jo vodijo klicateljeve nastavitve zaščite, BeginDoc pa te nastavitve po prehodu dešifriranja obnovi, da se šifriran vhod ne spremeni tiho v šifriran izhod
Politika vsebnikov se prebere iz slovarja /Encrypt, preden se poskusi katero koli geslo. Za /V 1 in 2 je vsak tok šifriran s ključem datoteke. Pri kriptografskih filtrih HotPDF /StmF razreši skozi /CF: filter Identity ali /CFM z vrednostjo None pomeni vsebnike v čistem besedilu, V2 in AESV2 pa šifrirane. Odgovor pristane v FReloadObjectStreamsEncrypted in je pomemben za en konkreten primer. Kadar so vsebniki v čistem besedilu, nizi pa ne, člani nosijo šifrirane nize, ki jih je treba dešifrirati posamično, zato MaterializeMembersOfPlaintextObjectStreams razširi vsakega stisnjenega člana pred prehodom dešifriranja po posameznih objektih. Ne naredi nič, kadar politika še ni znana, in nič, kadar so bili šifrirani vsebniki sami, ker so bili člani šifriranega vsebnika že dešifrirani z njim in ne smejo biti nikoli dešifrirani dvakrat
Kaj se zgodi, ko vsebnika ni mogoče dešifrirati?
Vsebnik, ki mu dešifriranje spodleti, gre v karanteno in ni usoden. Prehod tipa 2 zabeleži vnos THPDFObjStmQuarantineInfo v FObjStmQuarantine s številko objekta vsebnika, THPDFObjStmQuarantineReason, diagnostičnim nizom in seznamom številk objektov članov, ki jih je medsebojni sklic usmeril vanj. osqrDecryptFailed se sproži v štirih različnih situacijah: nobenega kriptografskega filtra ni bilo mogoče razrešiti, dešifriranje AES-256 ali AES-GCM je sprožilo izjemo, dešifriranje starega RC4 ali AES-128 je sprožilo izjemo ali pa uporabnega ključa datoteke sploh ni. Neodvisni vsebniki se nalagajo naprej, zato se dokument z enim poškodovanim vsebnikom še vedno odpre in še vedno upodobi vsako stran, ki od njega ni odvisna
Seznam karantene preživi nadomestno pot razčlenjevalnika. Če primarno nalaganje medsebojnih sklicev spodleti in HotPDF objektno tabelo rekonstruira s pregledovanjem datoteke, zastavica šifriranja iz prvega poskusa morda ne preživi te rekonstrukcije, zapisi karantene pa jo preživijo. Zato BeginDoc preverja seznam karantene in ne zastavice šifriranja: na naloženem dokumentu prehodi FObjStmQuarantine in se ustavi na prvem vnosu osqrDecryptFailed, pri čemer poimenuje vsebnik in zahteva ponovno nalaganje z veljavnim geslom. Prepis, ki bi šel naprej od te točke, bi člane, ki naj bi jih vsebnik držal, zapisal kot prazne objekte in poročal uspeh. Isti preizkus lahko poženete sami, prej in s svojo politiko, skozi javne dostopnike:
var
Info: THPDFObjStmQuarantineInfo;
I: Integer;
begin
Pdf.LoadFromFile('vendor-form.pdf'); // prazno uporabniško geslo
for I := 0 to Pdf.GetLoadedQuarantinedObjStmCount - 1 do
if Pdf.GetLoadedQuarantinedObjStmInfo(I, Info) and
(Info.Reason = osqrDecryptFailed) then
raise Exception.CreateFmt(
'Object stream %d is unreadable (%s); %d members unresolved',
[Info.ContainerObjNum, String(Info.Diagnostic),
Length(Info.MemberObjNums)]);
// od tu naprej je prepis varen
end;
Drugi razlogi za karanteno pokrivajo nekriptografske odpovedi: vsebnik, ki ni tok, manjkajoč slovar, neveljaven /N ali /First, velikost toka zunaj sprejetega obsega, odpoved razširjanja, /First, ki kaže čez podatke, ali telo člana, ki se je dekodiralo, a se ni razčlenilo. Te velja ob prevzemu zabeležiti v dnevnik, saj vsak poimenuje natanko tiste člane, ki vam bodo dolvodno manjkali
Zakaj prepis potrebuje izvirni številski žeton?
HotPDF vsak številski objekt shrani kot Single, Single pa ne more obnoviti izvornega besedila realnega števila. ISO 32000-1 §7.3.3 pisalcu dovoljuje, da za isto vrednost izda 0.750000, .75 ali 0.75, in nobeno od tega ne preživi poti skozi 24-bitno dvojiško obliko in splošni oblikovalnik nespremenjeno. Še slabše, vrednost, kot je 0.7, v Single sploh ni predstavljiva; razčleni se v najbližje plavajoče število in ponovno oblikovanje tega števila lahko da 0.69999999 ali zaokroženega soseda, odvisno od zanke po števkah. Pri barvi polnjenja ali konstanti prosojnosti /CA je to razlika enega koraka v 8-bitnem kanalu, kar zadostuje, da primerjava pik z izvorom pade, na mejah prelivov pa, da se razlika vidi
THPDFNumericObject.RememberSourceToken to reši za nespremenjeni primer. Razčlenjevalnik ga pokliče s surovim žetonom takoj po dodelitvi Value; metoda sprejme le žetone iz števk, največ ene decimalne pike in neobveznega vodilnega predznaka ter žeton shrani skupaj z vrednostjo, ki ji je ustrezal, v FSourceValue. Lastnost SourceToken shranjeno besedilo vrne le, dokler je Value še enak FSourceValue. Spremenite število in žeton izhlapi, zato spremenjena vrednost vedno gre skozi obstoječo pot oblikovanja in nikoli ne izda zastarelega besedila. SaveNumericObject najprej preveri SourceToken in ga, kadar obstaja, zapiše dobesedno, nato pa se na veje za cela števila, sklice barvnih prostorov in ulomke spusti le pri številih, ki so bila ustvarjena ali urejena v pomnilniku
Nespremenljivka je majhna in jo velja povedati naravnost: število, ki se ga niste dotaknili, se zapiše z bajti, s katerimi je bilo prebrano, število, ki ste se ga dotaknili, pa zapiše HotPDF-ov lastni oblikovalnik. Stisnjeni člani imajo od tega enako korist kot objekti iz telesa datoteke, saj EnsureCompressedObjectLoaded požene isti razčlenjevalnik po izrezu člana. Oblikovanje števil samo in njegova neodvisnost od področja procesa sta obravnavana v članku o invariantnem oblikovanju števil PDF v HotPDF
Preizkušanje poti prepisa proti tokovom objektov
Tri preverjanja ujamejo vsako od zgoraj opisanih odpovedi in nobeno od njih ne potrebuje Acrobata. Prvič, po shranjevanju primerjajte IndexedObjectCount z MaterializedObjectCount; pri polnem prepisu morata biti enaka in vsaka vrzel je član, ki je bil izpuščen. Drugič, v obeh datotekah izvlecite besedilo in naštejte drevo strukture, ne le upodobite ju, da se izgubljeni ActualText ali izgubljeni element strukture pokaže kot razlika. Tretjič, izhod naložite s svežo instanco in zahtevajte, da je GetLoadedQuarantinedObjStmCount enak nič, kar hkrati dokaže, da pisec ni izdelal vsebnika, ki ga bralec ne more odpreti. Kombinacije kriptografskih filtrov, ki odločajo o FReloadObjectStreamsEncrypted, so razložene v članku o politikah StmF, StrF in EFF. Pisalna stran te zgodbe, kako izdati tokove objektov in kdaj dati prednost inkrementalni posodobitvi pred prepisom, je v vodniku po tokovih objektov in inkrementalnih posodobitvah
Lenobno nalaganje članov, prehod razširitve pred pisalcem, karantena dešifriranja in ohranjanje izvirnih žetonov se vsi dobavljajo v HotPDF Delphi Component za Delphi in C++Builder. Stran izdelka povezuje referenco API, če želite GetLoadedObjectStreamCacheInfo in dostopnike karantene izslediti proti svoji prevzemni verigi