Kun HotPDF Delphi Component lataa PDF 1.5 -tiedoston LoadFromFile-kutsulla, se ei jäsennä /Type /ObjStm -säiliöiden sisään pakattuja objekteja. Se kirjaa ylös, missä kukin pakattu jäsen elää, ja jäsentää sen vasta kun jokin sitä pyytää. Juuri tuo laiska invariantti pitää latausajan verrannollisena siihen, mitä oikeasti kosketat, ja se on myös syy siihen, miksi täysuudelleenkirjoituksella on yksi ylimääräinen tehtävä ennen kuin tavuja lähtee ulos: laajenna jokainen yhä jäsentämätön jäsen, koska uudelleenkirjoitus on juuri heittämässä pois ne säiliöt, joissa ne jäsenet elävät
Oire, joka tämän muistiinpanon laukaisi, on helppo kuvata ja epämiellyttävä debugata. Lataa tiedosto, jonka fontit, väriavaruudet ja jäsennyspuu istuvat objektistreameissa, aja se BeginDoc- ja EndDoc -generointiparin läpi, niin tuloste avautuu valittamatta. Sivumäärä on oikea, teksti näkyy sivuilla joita pistokoemaisesti katsot. Sitten kollega avaa sivun 40 ja leipäteksti renderöityy korvatulla fontilla, tai Extract Text -komento palauttaa roskaa siinä missä ennen oli ActualText-korvaus. Mikään ei kaatunut. Kirjoittaja vain serialisoi objektin, jota ei koskaan ladattu, ja lataamaton objekti serialisoituu tyhjänä
Mitä LoadFromFile oikeastaan tallettaa pakatusta objektista?
Jokaiselle tyypin 2 ristiviitemerkinnälle LoadFromFile pitää pientä tietuetta FCompactObjects-kokoelmassa: objektinumero, säiliön sisältävän streamin indeksi säiliötaulukossa, jäsenen sijainti tuon streamin sisällä sekä ParsedObject-osoitin, joka alkaa nil-arvosta. Itse säiliö paikannetaan, puretaan salauksesta jos asiakirja on salattu, ja inflatoidaan, mutta jäsenten rungot jätetään tavuiksi. ISO 32000-1 §7.5.7 määrittelee säiliön asettelun, joka tekee tämän mahdolliseksi: otsikko, jossa on objektinumeron ja siirtymän pareja, ja sitten jäsenten rungot peräkkäin /First-arvon jälkeen, joten mikä tahansa yksittäinen jäsen voidaan leikata irti naapureihinsa koskematta
EnsureCompressedObjectLoaded on ainoa polku, joka muuttaa tietueen objektiksi. Se etsii tietueen objektinumeron perusteella, ja jos ParsedObject on jo asetettu, se palauttaa tuon välimuistissa olevan objektin ja laskee välimuistiosuman. Muussa tapauksessa se lataa säiliön uudelleen, jos se on häädetty, laskee jäsenen tavualueen siirtymätaulukosta, antaa jäsentimelle nollakopioivan näkymän tuosta siivusta ja tallentaa tuloksen takaisin tietueeseen. Siitä eteenpäin objekti on epäsuora, kantaa oikeaa objektinumeroaan ja on rekisteröity asiakirjan objekti-indeksiin kuin mikä tahansa tiedoston rungosta jäsennetty objekti. Luettelo, tietosanakirja, sivupuun juuri ja sivuobjektit kulkevat tämän polun läpi latausvaiheessa, koska navigointi tarvitsee niitä. Fontit, väriavaruudet, ExtGState-sanakirjat ja rakennealkiot eivät, ja ne pysyvät tietueina kunnes sivun renderöinti tai uudelleenkirjoitus koskee niihin
Tätä voi seurata ulkopuolelta. GetLoadedObjectStreamCacheInfo kertoo, kuinka monta säiliötä on olemassa, kuinka monta jäsentä indeksoitiin ja kuinka moni niistä on toistaiseksi jäsennetty:
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;
Rakennepainotteisella tiedostolla kolmas luku on pieni murto-osa toisesta heti latauksen jälkeen. Tuo ero on koko laiskan latauksen pointti, ja se on myös täsmälleen se objektijoukko, jonka perään täysuudelleenkirjoituksen on palattava
Miksi täysuudelleenkirjoitus pudottaa fontit, jotka inkrementaalinen tallennus säilyttää?
Täysuudelleenkirjoitus hylkää lähdetiedoston /ObjStm- ja /XRef-säiliöt ja serialisoi objektigraafin alusta asti, joten millään jäsenellä, jonka ParsedObject on yhä nil, ei ole enää esitystä tulosteessa. Inkrementaalisessa päivityksessä tätä ongelmaa ei koskaan ole, koska se liittää uudet objektit alkuperäisten tavujen perään ja jättää vanhat säiliöt paikalleen edellisen ristiviiteosion osoitettaviksi. Ero ei ole siinä, miten nämä kaksi tilaa kohtelevat fontteja. Se on siinä, jäävätkö alkuperäiset säiliöt henkiin seuraavan katselimen luettaviksi
Korjaus asuu SaveToStream-metodissa, siinä serialisoijassa jota EndDoc ajaa riippumatta siitä, asetatko FileName- vai OutputStream-kentän. Ennen kuin se hajauttaa mihinkään kirjoittajahaaraan, se käy läpi FCompactObjects-kokoelman ja kutsuu EnsureCompressedObjectLoaded-metodia jokaiselle merkinnälle. Jos jäsentä ei voi ladata, tallennus nostaa virheen sen sijaan että jatkaisi, koska uudelleenkirjoitus, joka pudottaa fonttisanakirjan hiljaa, on huonompi kuin sellainen joka pysähtyy. Laajennuksen on sijaittava tuolla tasolla, klassisen, pakatun ja linearisoidun haaran yläpuolella sekä linearisoidun reitin uudelleenladattujen rakennevirtojen karsinnan yläpuolella. Aiempi versio laajensi jäsenet vain SaveLoadedDocument-metodin sisällä, mikä kattoi ladatun asiakirjan sanaston ja ohitti generointisanaston kokonaan. LoadFromFile ja sen perään BeginDoc, sivumuokkaukset ja EndDoc menivät suoraan kirjoittajalle jokaisen koskemattoman jäsenen ollessa yhä jäsentämätön
// Molemmat uudelleenkirjoitussanastot laajentavat nyt pakatut jäsenet ennen kuin yksikään kirjoittaja ajaa.
// Ladatun asiakirjan polku:
Pdf.LoadFromFile('quarterly.pdf');
Pdf.SaveLoadedDocument('quarterly-rewritten.pdf');
// Generointipolku ladatun tiedoston yli:
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 materialisoi ensin jokaisen FCompactObjects-merkinnän
Välimuistissa olevat jäsenet pitävät sen, mitä niille teit. Objekti, joka jäsennettiin, muokattiin ja merkittiin likaiseksi ennen tallennusta, palautetaan välimuistista muokkauksineen, ja poistamasi jäsen pitää poistotilansa toistuvien tallennusten yli. Laajennuskierros on rakenteellisesti idempotentti: se täyttää aina vain nil-paikkoja
Miksi pikselitarkistukset kolmella sivulla eivät nappaa ActualText-tapausta
Rakennealkiot ovat se paikka, jossa tämä bugi piiloutuu pisimpään. Merkityn sisällön sekvenssissä oleva ActualText-merkintä, joka määritellään ISO 32000-1 §14.9.4:ssä, korvaa glyyfit poimintaa ja saavutettavuutta varten mutta ei vaikuta renderöintiin. Jos rakennealkio elää objektistreamissa ja uudelleenkirjoitus hukkaa sen, sivu piirtyy silti oikein, ensimmäinen, keskimmäinen ja viimeinen sivu vertautuvat pikseli pikseliltä lähteeseen, ja regressio näkyy vasta kun joku ajaa tekstinpoiminnan tai ruudunlukijan. Uudelleenkirjoitustesti, joka vain renderöi sivuja, ei ole merkittyä PDF:ää koskeva uudelleenkirjoitustesti. Vertaile myös poimittu teksti ja jäsennyspuu
Miten tyhjä käyttäjäsalasana muuttaa latausta?
Tyhjä käyttäjäsalasana tarkoittaa silti, että tiedosto on salattu, ja objektistreamit ovat sellaisessa tiedostossa salatekstiä kunnes tiedostoavain on palautettu. ISO 32000-1 §7.6.3.4 algoritmi 2 johtaa tuon avaimen salasanasta, /O-merkinnästä, /P-arvosta ja ensimmäisestä asiakirjatunnisteesta, ja HotPDF:n on ajettava se tyhjää merkkijonoa vasten ennen kuin tyypin 2 läpikäynti voi inflatoida yhtäkään säiliötä. Sen takia BeginDoc kutsuu ladatulla salatulla asiakirjalla DecryptLoadedDocument-metodia tyhjällä salasanalla ennen mitään muuta: objektigraafi on todennettava ja purettava salauksesta ennen kuin uudelleenkirjoitus voi alkaa, riippumatta siitä aikooko kutsuja suojata tulosteen. Tulosteen salaus on erillinen päätös, jota ohjaavat kutsujan suojausasetukset, ja BeginDoc palauttaa nuo asetukset salauksenpurkukierroksen jälkeen, jottei salattu syöte muutu hiljaa salatuksi tulosteeksi
Säiliöpolitiikka luetaan /Encrypt-sanakirjasta ennen kuin yhtäkään salasanaa kokeillaan. Arvoilla /V 1 ja 2 jokainen stream on salattu tiedostoavaimella. Krypt-suodattimien osalta HotPDF ratkaisee /StmF-arvon /CF-sanakirjan kautta: Identity-suodatin tai /CFM-arvo None tarkoittaa selväkielisiä säiliöitä, kun taas V2 ja AESV2 tarkoittavat salattuja. Vastaus päätyy FReloadObjectStreamsEncrypted-kenttään, ja sillä on merkitystä yhdessä tietyssä tapauksessa. Kun säiliöt ovat selväkielisiä mutta merkkijonot eivät, jäsenet kantavat salattuja merkkijonoja, jotka on purettava yksitellen, joten MaterializeMembersOfPlaintextObjectStreams laajentaa jokaisen pakatun jäsenen ennen objektikohtaista salauksenpurkukierrosta. Se ei tee mitään, kun politiikka ei ole vielä tiedossa, eikä mitään, kun säiliöt itsessään oli salattu, koska salatun säiliön jäsenet on jo purettu sen mukana eikä niitä saa koskaan purkaa kahdesti
Mitä tapahtuu, kun säiliötä ei voi purkaa salauksesta?
Säiliö, jonka salauksenpurkku epäonnistuu, asetetaan karanteeniin eikä se ole kohtalokasta. Tyypin 2 läpikäynti tallentaa THPDFObjStmQuarantineInfo-merkinnän FObjStmQuarantine-kokoelmaan säiliön objektinumerolla, THPDFObjStmQuarantineReason-syyllä, diagnostiikkamerkkijonolla sekä luettelolla niistä jäsenobjektien numeroista, jotka ristiviite oli ohjannut siihen. osqrDecryptFailed nostetaan neljässä eri tilanteessa: yhtään krypt-suodatinta ei voitu ratkaista, AES-256- tai AES-GCM-purku heitti poikkeuksen, perinteinen RC4- tai AES-128-purku heitti poikkeuksen, tai käyttökelpoista tiedostoavainta ei ole lainkaan. Riippumattomat säiliöt jatkavat latautumista, joten asiakirja, jossa on yksi vahingoittunut säiliö, avautuu edelleen ja renderöi edelleen jokaisen sivun, joka ei ole siitä riippuvainen
Karanteenilista selviää jäsentimen varamenettelystä. Jos ensisijainen ristiviitteen lataus epäonnistuu ja HotPDF rekonstruoi objektitaulukon skannaamalla tiedoston, ensimmäisen yrityksen salauslippu ei välttämättä selviä tuosta rekonstruktiosta, mutta karanteenimerkinnät selviävät. Sen takia BeginDoc tarkistaa karanteenilistan eikä salauslippua: ladatulla asiakirjalla se käy läpi FObjStmQuarantine-kokoelman ja nostaa virheen ensimmäisestä osqrDecryptFailed-merkinnästä nimeämällä säiliön ja pyytämällä uudelleenlatausta kelvollisella salasanalla. Uudelleenkirjoitus, joka etenisi tuon pisteen ohi, kirjoittaisi ne jäsenet, jotka säiliön oli määrä sisältää, tyhjinä objekteina ja ilmoittaisi onnistumisesta. Voit ajaa saman tarkistuksen itse, aikaisemmin ja omalla politiikallasi, julkisten käsittelijöiden kautta:
var
Info: THPDFObjStmQuarantineInfo;
I: Integer;
begin
Pdf.LoadFromFile('vendor-form.pdf'); // tyhjä käyttäjäsalasana
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)]);
// tästä eteenpäin uudelleenkirjoitus on turvallinen
end;
Muut karanteenisyytt kattavat ei-kryptografiset viat: säiliö, joka ei ole stream, puuttuva sanakirja, virheellinen /N tai /First, hyväksytyn alueen ulkopuolella oleva streamin koko, pakkauksen purun epäonnistuminen, datan ohi osoittava /First tai jäsenen runko, joka purkautui mutta ei jäsentynyt. Ne kannattaa lokittaa vastaanotossa, sillä jokainen niistä nimeää täsmälleen ne jäsenet, jotka sinulta jäävät puuttumaan alavirrassa
Miksi uudelleenkirjoitus tarvitsee alkuperäisen numeerisen tokenin?
HotPDF tallentaa jokaisen numeerisen objektin Single-arvona, eikä Single pysty toistamaan reaaliluvun lähdetekstiä. ISO 32000-1 §7.3.3 sallii kirjoittajan tuottaa samalle arvolle muodot 0.750000, .75 tai 0.75, eikä yksikään niistä selviä muuttumattomana edestakaisesta matkasta 24-bittisen binääriesityksen ja yleiskäyttöisen muotoilijan läpi. Pahempaa: arvo kuten 0.7 ei ole Single-arvossa esitettävissä lainkaan; se jäsentyy lähimmäksi liukuluvuksi, ja tuon liukuluvun uudelleenmuotoilu voi tuottaa muodon 0.69999999 tai pyöristetyn naapurin numeroluupista riippuen. Täyttövärissä tai /CA-läpinäkyvyysvakiossa se on yhden askelen ero 8-bittisessä kanavassa, mikä riittää kaatamaan pikselivertailun lähdettä vasten ja liukuvärien rajoilla riittää näkymään
THPDFNumericObject.RememberSourceToken ratkaisee tämän muokkaamattoman tapauksen osalta. Jäsennin kutsuu sitä raa'alla tokenilla heti Value-arvon sijoittamisen jälkeen; metodi hyväksyy vain tokeneita, jotka koostuvat numeroista, enintään yhdestä desimaalipisteestä ja valinnaisesta etumerkistä, ja tallentaa tokenin yhdessä sitä vastanneen arvon kanssa FSourceValue-kenttään. SourceToken-ominaisuus palauttaa tallennetun tekstin vain niin kauan kuin Value on yhä yhtä suuri kuin FSourceValue. Muuta lukua, niin token haihtuu, joten muokattu arvo kulkee aina olemassa olevan muotoilupolun läpi eikä koskaan tuota vanhentunutta tekstiä. SaveNumericObject tarkistaa SourceToken-arvon ensin ja kirjoittaa sen sellaisenaan kun se on läsnä, ja putoaa sitten kokonaisluku-, väriavaruusviite- ja murtolukuharoihin vain niiden lukujen osalta, jotka luotiin tai muokattiin muistissa
Invariantti on pieni ja kannattaa todeta suoraan: luku, johon et koskenut, kirjoitetaan niillä tavuilla joilla se luettiin, ja luku, johon koskit, kirjoitetaan HotPDF:n omalla muotoilijalla. Pakatut jäsenet hyötyvät tästä samalla tavalla kuin rungon objektit, koska EnsureCompressedObjectLoaded ajaa saman jäsentimen jäsenen siivun yli. Itse lukujen muotoilu ja sen riippumattomuus prosessin locale-asetuksesta on käsitelty artikkelissa locale-riippumattomasta PDF-lukujen muotoilusta HotPDF:ssä
Uudelleenkirjoituspolun testaaminen objektistreameja vasten
Kolme tarkistusta nappaa jokaisen yllä kuvatun vian, eikä yksikään niistä vaadi Acrobatia. Ensimmäinen: vertaa IndexedObjectCount-arvoa MaterializedObjectCount-arvoon tallennuksen jälkeen; täysuudelleenkirjoituksessa niiden on oltava yhtä suuret, ja mikä tahansa ero on pudotettu jäsen. Toinen: poimi teksti ja luettele jäsennyspuu molemmista tiedostoista, älä vain renderöi niitä, jotta kadonnut ActualText tai kadonnut rakennealkio näkyy diffinä. Kolmas: lataa tuloste tuoreella instanssilla ja varmista, että GetLoadedQuarantinedObjStmCount on nolla, mikä myös todistaa, ettei kirjoittaja tuottanut säiliötä, jota lukija ei voi avata. Krypt-suodatinyhdistelmät, jotka ratkaisevat FReloadObjectStreamsEncrypted-arvon, on esitetty StmF-, StrF- ja EFF-politiikka-artikkelissa. Tämän tarinan kirjoittajapuoli, eli miten objektistreamit tuotetaan ja milloin inkrementaalinen päivitys kannattaa valita uudelleenkirjoituksen sijaan, on objektistream- ja inkrementaalipäivitysoppaassa
Laiska jäsenlataus, kirjoittajaa edeltävä laajennuskierros, salauksenpurun karanteeni ja lähdetokenin säilytys toimitetaan kaikki HotPDF Delphi Componentissa Delphille ja C++Builderille. Tuotesivulla on linkki rajapintaviitteeseen, jos haluat jäljittää GetLoadedObjectStreamCacheInfo-kutsun ja karanteenikäsittelijät omaa vastaanottoputkeasi vasten