Techninis straipsnis

Tingus PDF ObjStm narių krovimas ir pilnas perrašymas Delphi

Kai HotPDF Delphi Component įkelia PDF 1.5 failą su LoadFromFile, jis neanalizuoja objektų, supakuotų /Type /ObjStm konteineriuose. Jis įrašo, kur gyvena kiekvienas suspaustas narys, ir analizuoja jį tik tada, kai kas nors jo paprašo. Būtent tas tingaus įkėlimo invariantas ir laiko įkėlimo laiką proporcingą tam, ką iš tikrųjų liečiate, ir jis taip pat yra priežastis, kodėl pilnas perrašymas turi atlikti vieną papildomą darbą, dar neišleidęs nė vieno baito: išplėsti kiekvieną dar neišanalizuotą narį, nes perrašymas tuoj išmes konteinerius, kuriuose tie nariai gyvena

Simptomą, kuris paskatino šį įrašą, lengva apibūdinti ir nemalonu derinti. Įkelkite failą, kurio šriftai, spalvų erdvės ir struktūros medis sėdi objektų srautuose, paleiskite jį per BeginDoc ir EndDoc generavimo porą, ir išvestis atsidaro be priekaištų. Puslapių skaičius teisingas, tekstas matomas tuose puslapiuose, kuriuos patikrinate. Tada kolega atidaro 40 puslapį ir pagrindinis tekstas atvaizduojamas pakeistu šriftu, arba komanda Extract Text grąžina šiukšles ten, kur anksčiau buvo ActualText pakaitalas. Niekas nekrito. Rašytojas tiesiog serializavo objektą, kuris niekada nebuvo įkeltas, o neįkeltas objektas serializuojasi kaip niekas

Ką LoadFromFile iš tikrųjų palieka suspaustam objektui?

Kiekvienam 2 tipo kryžminės nuorodos įrašui LoadFromFile palieka nedidelį įrašą FCompactObjects masyve: objekto numerį, talpinančio srauto indeksą konteinerių lentelėje, nario poziciją tame sraute ir ParsedObject rodyklę, kuri prasideda kaip nil. Pats konteineris surandamas, iššifruojamas, jei dokumentas užšifruotas, ir išpučiamas, bet narių kūnai paliekami kaip baitai. ISO 32000-1 §7.5.7 apibrėžia konteinerio išdėstymą, kuris tai padaro įmanoma: antraštė iš objekto numerio ir poslinkio porų, tada narių kūnai, sujungti po /First, tad bet kuris atskiras narys gali būti iškirptas neliečiant jo kaimynų

EnsureCompressedObjectLoaded yra vienintelis kelias, kuris įrašą paverčia objektu. Jis suranda įrašą pagal objekto numerį ir, jei ParsedObject jau nustatytas, grąžina tą podėlyje esantį objektą ir užskaito podėlio pataikymą. Kitu atveju jis iš naujo įkelia konteinerį, jei šis buvo iškeltas, apskaičiuoja nario baitų intervalą iš poslinkių lentelės, perduoda analizatoriui nulinės kopijos to fragmento vaizdą ir įrašo rezultatą atgal į įrašą. Nuo tada objektas yra netiesioginis, nešasi savo tikrąjį objekto numerį ir yra užregistruojamas dokumento objektų indekse kaip bet kuris objektas, išanalizuotas iš failo kūno. Katalogas, info žodynas, puslapių medžio šaknis ir puslapių objektai įkėlimo metu eina šiuo keliu, nes navigacijai jų reikia. Šriftai, spalvų erdvės, ExtGState žodynai ir struktūros elementai ne, ir jie lieka įrašais, kol juos paliečia puslapio atvaizdavimas ar perrašymas

Kaip HotPDF Delphi Component laiko suspaustą narį, kol jis dar neišanalizuotas: FCompactObjects įrašas laiko objekto numerį, konteinerio indeksą, nario indeksą ir nulinę ParsedObject rodyklę, o EnsureCompressedObjectLoaded įrašą paverčia užregistruotu objektu per podėlio pataikymus, konteinerių įkėlimą iš naujo, fragmentavimą pagal poslinkių lentelę ir nulinės kopijos analizę
LoadFromFile palieka /ObjStm narių kūnus kaip baitus ir analizuoja juos tik tada, kai skaitytuvas paprašo, tad įkėlimo laikas seka tai, ką liečiate – katalogas ir puslapių medis ateina anksti, o šriftai, spalvų erdvės ir struktūros elementai lieka įrašais

Tai galite stebėti iš išorės. GetLoadedObjectStreamCacheInfo praneša, kiek konteinerių egzistuoja, kiek narių buvo indeksuota ir kiek iš jų jau išanalizuota:

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;

Su struktūra turtingame faile trečiasis skaičius tuoj po įkėlimo yra maža antrojo dalis. Būtent ta spraga ir yra visas tingaus įkėlimo tikslas, ir ji taip pat yra lygiai ta objektų aibė, dėl kurios pilnas perrašymas turi grįžti

Kodėl pilnas perrašymas numeta šriftus, kuriuos inkrementinis išsaugojimas išlaiko?

Pilnas perrašymas išmeta šaltinio failo /ObjStm ir /XRef konteinerius ir iš naujo serializuoja objektų grafą, tad bet kuris narys, kurio ParsedObject vis dar nil, nebeturi jokios reprezentacijos išvestyje. Inkrementinis atnaujinimas šios problemos niekada neturi, nes jis prideda naujus objektus po originalių baitų ir palieka senus konteinerius vietoje, kad juos adresuotų ankstesnė kryžminės nuorodos sekcija. Skirtumas ne tame, kaip abu režimai elgiasi su šriftais. Jis tame, ar originalūs konteineriai išlieka, kad juos perskaitytų kita peržiūros programa

Pataisa gyvena SaveToStream, serializatoriuje, kurį EndDoc varo nepriklausomai nuo to, ar nustatote FileName, ar OutputStream. Prieš perduodamas valdymą bet kuriai rašytojo šakai, jis pereina FCompactObjects ir kiekvienam įrašui iškviečia EnsureCompressedObjectLoaded. Jei nario įkelti nepavyksta, išsaugojimas kelia klaidą, o ne tęsia, nes perrašymas, kuris tyliai numeta šrifto žodyną, yra blogesnis už tą, kuris sustoja. Išplėtimas turi sėdėti tame lygyje, virš klasikinės, supakuotos ir tiesinės šakų bei virš tiesinio kelio atliekamo iš naujo įkeltų struktūrinių srautų išvalymo. Ankstesnė versija narius išplėsdavo tik SaveLoadedDocument viduje, kas uždengė įkelto dokumento žodyną ir visiškai praleido generavimo žodyną. LoadFromFile ir po jo BeginDoc, puslapio redagavimai bei EndDoc ėjo tiesiai į rašytoją su kiekvienu nepaliestu nariu vis dar neišanalizuotu

Kur sėdi HotPDF pilno perrašymo išplėtimas: SaveToStream pereina kiekvieną FCompactObjects įrašą per EnsureCompressedObjectLoaded prieš perduodamas valdymą klasikiniam, supakuotam ar tiesiniam rašytojui, tad ir SaveLoadedDocument žodynas, ir LoadFromFile bei BeginDoc bei EndDoc žodynas serializuoja pilnai išanalizuotus objektus, o ne nulinius įrašus
Inkrementinis atnaujinimas prideda po originalių baitų ir palieka senus konteinerius perskaitomus, o pilnas perrašymas juos išmeta – vienas išplėtimo praėjimas virš kiekvienos rašytojo šakos ir neleidžia neįkeltam šriftui ar struktūros elementui nusiserializuoti kaip niekui
// Abu perrašymo žodynai dabar išplečia kompaktinius narius prieš paleidžiant rašytoją.
// Įkelto dokumento kelias:
Pdf.LoadFromFile('quarterly.pdf');
Pdf.SaveLoadedDocument('quarterly-rewritten.pdf');

// Generavimo kelias per įkeltą failą:
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 pirmiausia materializuoja kiekvieną FCompactObjects įrašą

Podėlyje esantys nariai išlaiko viską, ką jiems padarėte. Objektas, kuris buvo išanalizuotas, paredaguotas ir pažymėtas nešvariu prieš išsaugojimą, grąžinamas iš podėlio su savo pakeitimais, o jūsų ištrintas narys išlaiko ištrynimo būseną per pakartotinius išsaugojimus. Išplėtimo praėjimas iš konstrukto yra idempotentinis: jis tik užpildo nil vietas

Kodėl trijų puslapių pikselių patikrinimai praleidžia ActualText atvejį

Struktūros elementai yra ten, kur šis defektas slepiasi ilgiausiai. ActualText įrašas žymėto turinio sekoje, apibrėžtas ISO 32000-1 §14.9.4, pakeičia glifus ištraukimui ir prieinamumui, bet neveikia atvaizdavimo. Jei struktūros elementas gyvena objektų sraute ir perrašymas jį praranda, puslapis vis tiek nubraižomas teisingai, pirmas, vidurinis ir paskutinis puslapiai sutampa su šaltiniu pikselis į pikselį, o regresija pasirodo tik tada, kai kas nors paleidžia teksto ištraukimą ar ekrano skaitytuvą. Perrašymo testas, kuris tik atvaizduoja puslapius, nėra žymėto PDF perrašymo testas. Palyginkite ir ištrauktą tekstą, ir struktūros medį

Kaip tuščias naudotojo slaptažodis pakeičia įkėlimą?

Tuščias naudotojo slaptažodis vis tiek reiškia, kad failas užšifruotas, o objektų srautai tokiame faile yra šifrotekstas, kol failo raktas neatkurtas. ISO 32000-1 §7.6.3.4 2 algoritmas išveda tą raktą iš slaptažodžio, /O įrašo, /P ir pirmojo dokumento identifikatoriaus, ir HotPDF turi jį paleisti su tuščia eilute, kol 2 tipo praėjimas gali išpūsti nors vieną konteinerį. Štai kodėl BeginDoc įkeltame užšifruotame dokumente pirmiausia iškviečia DecryptLoadedDocument su tuščiu slaptažodžiu: objektų grafas turi būti autentifikuotas ir iššifruotas dar prieš prasidedant perrašymui, nepriklausomai nuo to, ar kviečiantysis ketina apsaugoti išvestį. Išvesties šifravimas yra atskiras sprendimas, varomas kviečiančiojo apsaugos nustatymų, ir BeginDoc atstato tuos nustatymus po iššifravimo praėjimo, kad užšifruota įvestis tyliai nevirstų užšifruota išvestimi

Konteinerių politika perskaitoma iš /Encrypt žodyno dar nebandant jokio slaptažodžio. Su /V 1 ir 2 kiekvienas srautas šifruojamas failo raktu. Kripto filtrų atveju HotPDF /StmF išsprendžia per /CF: Identity filtras arba None reikšmė /CFM lauke reiškia atviro teksto konteinerius, o V2 ir AESV2 reiškia užšifruotus. Atsakymas nusileidžia į FReloadObjectStreamsEncrypted, ir jis svarbus vienam konkrečiam atvejui. Kai konteineriai yra atviras tekstas, o eilutės ne, nariai nešasi užšifruotas eilutes, kurias reikia iššifruoti atskirai, tad MaterializeMembersOfPlaintextObjectStreams išplečia kiekvieną kompaktinį narį prieš kiekvieno objekto iššifravimo praėjimą. Jis nedaro nieko, kai politika dar nežinoma, ir nieko, kai patys konteineriai buvo užšifruoti, nes užšifruoto konteinerio nariai jau buvo su juo iššifruoti ir niekada neturi būti iššifruojami antrą kartą

Kas atsitinka, kai konteinerio iššifruoti nepavyksta?

Konteineris, kurio iššifruoti nepavyksta, yra karantinuojamas, o ne mirtinas. 2 tipo praėjimas įrašo THPDFObjStmQuarantineInfo įrašą FObjStmQuarantine su konteinerio objekto numeriu, THPDFObjStmQuarantineReason priežastimi, diagnostine eilute ir narių objektų numerių sąrašu, kuriuos kryžminė nuoroda buvo nukreipusi į jį. osqrDecryptFailed keliama keturioms skirtingoms situacijoms: nepavyko išspręsti jokio kripto filtro, AES-256 arba AES-GCM iššifravimas metė klaidą, senasis RC4 arba AES-128 iššifravimas metė klaidą, arba jokio tinkamo failo rakto apskritai nėra. Nepriklausomi konteineriai įkeliami toliau, tad dokumentas su vienu sugadintu konteineriu vis tiek atsidaro ir vis tiek atvaizduoja kiekvieną puslapį, kuris nuo jo nepriklauso

Kaip HotPDF iššifravimo karantinas veikia įkeltame PDF: konteineris, kurio iššifravimas meta klaidą, įrašomas kaip THPDFObjStmQuarantineInfo su osqrDecryptFailed priežastimi ir savo narių objekto numeriais, nepriklausomi konteineriai įkeliami toliau, o BeginDoc kelia klaidą ties pirmu nepavykusiu įrašu, kol perrašymas dar gali pranešti apie sėkmę
Karantino įrašai išgyvena analizatoriaus atsarginį kelią, o BeginDoc juos tikrina pagal vardą, o ne pagal užšifravimo vėliavėlę, tad dokumentas su vienu sugadintu konteineriu vis tiek atsidaro, o perrašymo kelias sustoja, o ne rašo tuščius objektus

Karantino sąrašas išgyvena analizatoriaus atsarginį kelią. Jei pirminis kryžminės nuorodos įkėlimas nepavyksta ir HotPDF rekonstruoja objektų lentelę skenuodamas failą, užšifravimo vėliavėlė iš pirmo bandymo tos rekonstrukcijos gali neišgyventi, o karantino įrašai išgyvena. Štai kodėl BeginDoc tikrina karantino sąrašą, o ne užšifravimo vėliavėlę: įkeltame dokumente jis pereina FObjStmQuarantine ir kelia klaidą ties pirmu osqrDecryptFailed įrašu, įvardydamas konteinerį ir prašydamas įkelti iš naujo su galiojančiu slaptažodžiu. Perrašymas, kuris nuėjęs toliau, įrašytų narius, kuriuos konteineris turėjo laikyti, kaip tuščius objektus, ir praneštų apie sėkmę. Tą patį patikrinimą galite atlikti patys, anksčiau ir su savo politika, per viešuosius prieigos metodus:

var
  Info: THPDFObjStmQuarantineInfo;
  I: Integer;
begin
  Pdf.LoadFromFile('vendor-form.pdf');   // tuščias naudotojo slaptažodis
  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)]);
  // nuo čia perrašymas saugus
end;

Kitos karantino priežastys uždengia ne kriptografinius gedimus: konteineris, kuris nėra srautas, trūkstamas žodynas, negaliojantis /N arba /First, srauto dydis už priimtino intervalo ribų, dekompresijos gedimas, /First, rodantis už duomenų, arba nario kūnas, kuris nusidekodavo, bet neišsianalizavo. Juos verta registruoti įvedant, nes kiekvienas įvardija būtent tuos narius, kurių trūks toliau

Kodėl perrašymui reikia originalaus skaitinio žetono?

HotPDF kiekvieną skaitinį objektą saugo kaip Single, o Single negali atkurti realiojo skaičiaus šaltinio teksto. ISO 32000-1 §7.3.3 leidžia rašytojui tam pačiam skaičiui išduoti 0.750000, .75 arba 0.75, ir nė vienas iš jų nepraeina kelionės per 24 bitų dvejetainį formatą ir bendrą formatuotoją nepakitęs. Dar blogiau, reikšmė kaip 0.7 Single formate apskritai neatskiriama; ji nusianalizuoja į artimiausią slankaus kablelio skaičių, o to skaičiaus performatavimas gali duoti 0.69999999 arba suapvalintą kaimyną, priklausomai nuo skaitmenų kilpos. Užpildymo spalvoje arba /CA permatomumo konstantoj tai yra vieno skaičiaus skirtumas 8 bitų kanale, ir to pakanka, kad kristų palyginimas su šaltiniu pikseliais, o gradientų ribose – kad būtų matoma

THPDFNumericObject.RememberSourceToken tai išsprendžia nepakeistam atvejui. Analizatorius iškviečia jį su neapdorotu žetonu tuoj po Value priskyrimo; metodas priima tik žetonus iš skaitmenų, daugiausiai vieno dešimtainio taško ir neprivalomo pradinio ženklo, ir įrašo žetoną kartu su reikšme, kurią jis atitiko, FSourceValue. SourceToken savybė grąžina įrašytą tekstą tik tol, kol Value vis dar lygi FSourceValue. Pakeiskite skaičių, ir žetonas išgaruoja, tad pakeista reikšmė visada eina per esamą formatavimo kelią ir niekada neišduoda pasenusio teksto. SaveNumericObject pirmiausia tikrina SourceToken ir, kai jis yra, įrašo jį pažodžiui, o prie sveikojo skaičiaus, spalvų erdvės nuorodos ir trupmeninės šakų grįžta tik skaičiams, kurie buvo sukurti ar paredaguoti atmintyje

Invariantas nedidelis ir vertas pasakyti tiesiai: skaičius, kurio nelietėte, įrašomas tais pačiais baitais, su kuriais buvo perskaitytas, o skaičius, kurį palietėte, įrašomas HotPDF pačios formatuotoju. Kompaktiniai nariai tuo naudojasi taip pat, kaip ir kūno objektai, nes EnsureCompressedObjectLoaded paleidžia tą patį analizatorių nario fragmentui. Pats skaičių formatavimas ir jo nepriklausomumas nuo proceso lokalės aprašyti straipsnyje apie nuo lokalės nepriklausantį PDF skaičių formatavimą HotPDF

Perrašymo kelio testavimas prieš objektų srautus

Trys patikrinimai pagauna kiekvieną aukščiau aprašytą gedimą, ir nė vienam jų nereikia Acrobat. Pirma, palyginkite IndexedObjectCount su MaterializedObjectCount po išsaugojimo; pilname perrašyme jie turi būti lygūs, o bet kokia spraga yra išmestas narys. Antra, ištraukite tekstą ir išvardinkite struktūros medį abiejuose failuose, o ne tik juos atvaizduokite, kad prarastas ActualText ar prarastas struktūros elementas pasirodytų kaip skirtumas. Trečia, įkelkite išvestį su šviežiu egzemplioriumi ir patikrinkite, kad GetLoadedQuarantinedObjStmCount yra nulis, kas taip pat įrodo, kad rašytojas nepagamino konteinerio, kurio skaitytuvas negali atidaryti. Kripto filtrų deriniai, lemiantys FReloadObjectStreamsEncrypted, išdėstyti StmF, StrF ir EFF politikos straipsnyje. Rašytojo pusė šioje istorijoje – kaip išduoti objektų srautus ir kada teikti pirmenybę inkrementiniam atnaujinimui vietoje perrašymo – yra objektų srautų ir inkrementinių atnaujinimų vadove

Tingus narių įkėlimas, prieš rašytoją einantis išplėtimo praėjimas, iššifravimo karantinas ir šaltinio žetono išsaugojimas visi platinami HotPDF Delphi Component, skirtoje Delphi ir C++Builder. Produkto puslapyje yra nuoroda į API dokumentaciją, jei norite atsekti GetLoadedObjectStreamCacheInfo ir karantino prieigos metodus prieš savo įvedimo grandinę