Atidarykite Microsoft Word arba Excel sukurtą PDF failą, pereikite per jo puslapius ir niekas neatrodys neįprasta. Įkelkite jį į Delphi programą, perskaitykite puslapių skaičių ir skaičius bus teisingas. Tada išsaugokite jį iš naujo su įjungtu šifravimu ir užduotis nepavyks su EListError klaida arba rezultatas bus atidarytas su įspėjimu apie pažeistą kryžminių nuorodų (cross-reference) lentelę. Failas niekada nebuvo sugadintas. Tai hibridinės nuorodos failas, ir būtent ta struktūra, kuri leidžia penkiolikos metų senumo peržiūros programai jį atidaryti, trukdo įkėlimo programai, kuri nustoja skaityti per anksti.
Tai yra vienas dažniausių būdų, kaip PDF apdorojimo grandinė, sėkmingai praėjusi visus vidinius testus, susiduria su failu, kurio negali pilnai apdoroti. Visi vidiniai testiniai failai buvo sugeneruoti vietoje, todėl jie niekada nebuvo hibridiniai. Pirmasis hibridinis failas pasirodo tą dieną, kai klientas persiunčia sąskaitą faktūrą, eksportuotą iš skaičiuoklės.
Ką iš tikrųjų įrašo „Word“ ir „Excel“
ISO 32000-1 aprašo hibridinių nuorodų išdėstymą §7.5.8.4. Programa, kuri nori naudoti PDF 1.5 funkcijas, pavyzdžiui, objektų srautus (object streams), bet kartu leidžia PDF 1.4 peržiūros programai atidaryti failą, kryžminių nuorodų informaciją įrašo du kartus. Yra klasikinė kryžminių nuorodų lentelė su fiksuoto pločio ASCII eilutėmis, kurios užbaigdavo kiekvieną PDF failą iki 1.4 versijos, ir yra kryžminių nuorodų srautas, kuris indeksuoja likusią dalį. Klasikinės dalies pabaigoje esantis trailer blokas turi /XRefStm įrašą, kurio reikšmė yra to srauto baitų poslinkis (offset).
Darbo pasidalijimas yra apgalvotas. Objektai, kuriuos turi pasiekti sena peržiūros programa, įskaitant katalogą ir puslapių medį, yra pasiekiami iš klasikinės lentelės. Objektai, kurie buvo suglaudinti į suspaustus objektų srautus, klasikinėje lentelėje pažymimi kaip laisvi naudojant f tipo įrašą, todėl 1.4 peržiūros programa juos praleidžia ir niekada nesusiduria su struktūra, kurios negali išanalizuoti. Tikrosios jų vietos saugomos tik kryžminių nuorodų sraute. Tokio failo požymis yra jo pabaiga: trumpa klasikinė dalis, dažnai ne kas kita, kaip xref, po kurio seka sekcijos antraštė 0 0, kurios pabaigos blokas rodo į /XRefStm, kur saugomi tikrieji duomenys.
Kodėl teisingas puslapių skaičius nieko neįrodo
Kadangi katalogas ir puslapių medis yra tikslingai pasiekiami iš klasikinės lentelės, įkėlimo programa, kuri nuskaito tik šią lentelę, suranda /Root, pereina puslapių medį ir praneša teisingą puslapių skaičių. Viskas, ko reikia senai peržiūros programai, yra prieinama, todėl failas atrodo tvarkingas. Dingę objektai yra tie, kurie buvo supakuoti į objektų srautus: AcroForm laukų žodynai, pažymėto PDF struktūros elementai bei daugybė kitų smulkių žodynų, kurie neturėjo būti matomi senesnėms programoms.
Šio trūkumo nepastebėsite tol, kol kas nors nepalies tų objektų, o pilnas failo išsaugojimas iš naujo paliečia juos visus. Perėjimas per dokumentą siekiant jį iš naujo užšifruoti arba perrašyti yra būtent ta operacija, kuri iš eilės reikalauja kiekvienog objekto numerio, todėl šis simptomas pasireiškia išsaugojimo, o ne įkėlimo metu, toli nuo tikrosios priežasties.
Spąstai yra detektorius, kuris pamato „xref“ ir sustoja
Paprastas būdas nuspręsti, kaip failas yra indeksuojamas, yra sekti startxref ir patikrinti pirmuosius baitus, į kuriuos jis rodo. Raktinis žodis xref reiškia klasikinę lentelę; srauto objektas reiškia kryžminių nuorodų srautą. Šis testas yra teisingas bet kuriam failui, kuris naudoja tik vieną schemą. Tačiau jis yra neteisingas hibridiniam failui, kurio startxref rodo į klasikinę dalį tik tam, kad patenkintų senas peržiūros programas, o tos dalies pabaigoje esantis /XRefStm yra vieta, kur iš tikrųjų yra suindeksuota didžioji dokumento dalis. Detektorius, kuris grąžina reikšmę „klasikinis“ vos pamatęs pirmąjį xref, niekada neperskaitys /XRefStm, todėl kiekvienas objektas, esantis tik sraute, taps nematomas.
var
Pdf: THotPDF;
PageCount: Integer;
begin
Pdf := THotPDF.Create(nil);
try
PageCount := Pdf.LoadFromFile('Invoice_XLS.pdf'); // count is correct
// inspect or edit the loaded document here
Pdf.SaveLoadedDocument('Invoice_secured.pdf'); // walks every object
finally
Pdf.Free;
end;
end;
Naudojant tokį detektorių, įkėlimas atrodo sėkmingas, o išsaugojimas iš naujo yra ta vieta, kur pasireiškia trūkstami objektai. Sprendimas yra ne skaityti daugiau baitų pradžioje, o atpažinti hibridinį trailer bloką ir sekti /XRefStm nuorodą prieš nusprendžiant, kad failas yra visiškai apdorotas.
Sujungimo tvarka yra privaloma
Perskaičius abu indeksus, jie gali būti sujungti tik viena kryptimi. Kryžminių nuorodų srautas turi būti sujungtas pirmiausia, o klasikiniai įrašai užpildomi aplink jį. Priežastis – nedidelė apgaulė pačiame formato pagrinde. Hibridinis failas pažymi savo suspaustus objektus kaip laisvus klasikinėje lentelėje, kad senos peržiūros programos juos ignoruotų. Įkėlimo programa, kuri vadovaujasi taisykle „pirmas pamatytas laimi“ ir pirmiausia nuskaito klasikinę lentelę, užregistruos tuos objektų numerius kaip laisvus, o tada atmes srauto įrašus, kurie iš tikrųjų nurodo jų vietą, nes vietos jau užimtos. Pakeiskite tvarką, ir 2 tipo įrašai iš srauto, kurių kiekvienas yra objektų srauto numeris plius indeksas, užims jiems skirtas vietas, o klasikiniai įrašai išsidėstys aplink juos.
Ta pati taisyklė apsaugo nuo to, kad senesnė versija neprikeltų ištrinto objekto. Papildomi atnaujinimai jungiasi atgal per /Prev nuorodą, o 0 tipo laisvas įrašas yra sargybinis, nurodantis, kad naujesnė sekcija pašalino objekto numerį. Vėlesnė, senesnė sekcija grandinėje neturi perrašyti šio sargybinio pasenusia vieta. Jei pirmąjį pamatytą įrašą traktuosite kaip autoritetingą laisviems žymekliams, ištrintas objektas liks ištrintas; jei elgsitės nerūpestingai, paties failo istorija gali prikelti turinį, kurį naujausia versija pašalino.
Ką tai reiškia „HotPDF“ komponente
Šis variklis išsprendžia hibridinių nuorodų failų problemą už jus, ir tai daroma kiekviename kelyje, kuriame reikia analizuoti kryžminių nuorodų duomenis. Įkelkite dokumentą naudodami LoadFromFile arba LoadFromStream, atlikite pakeitimus ir iškvieskite SaveLoadedDocument; arba paleiskite vienkartinę operaciją, pavyzdžiui, EncryptFile, kuri nuskaito įvestį ir įrašo išvestį. Bet kuriuo atveju atkūrimo procesas nuskaito /XRefStm, sujungia srauto sekciją prieš klasikinius įrašus ir išsprendžia srautuose esančius objektus prieš juos išvardijant įrašymo programai. Problema pirmiausia išryškėjo AES-256 šifravimo kelyje, nes dokumento šifravimas perrašo kiekvieną objektą, todėl reikalauja, kad kiekvienas objektas jau būtų surastas.
// One-shot: read the hybrid input, write an AES-256 encrypted copy
Pdf.EncryptFile('Letter_DOC.pdf', 'Letter_secured.pdf',
'owner-secret', '', aes256, [prPrint, prFillAnnotations]);
Detalė, kurią verta įsidėmėti, yra prieš API naudojimą. Failai, gauti iš Word, Excel, PowerPoint ir daugelio kitų „Išsaugoti kaip PDF“ programų, reguliariai yra hibridiniai, todėl įkėlimo programa, kurią testuojate tik su savo kodo sugeneruota išvestimi, testavimo metu gali su jais niekada nesusidurti. Papildykite savo testavimo failų bazę dokumentais, eksportuotais iš tikrųjų Office programų, o ne tik failais, kuriuos sukūrė jūsų pačių kodas.
Įtartino failo tikrinimas
Du patikrinimai greitai išsprendžia šį klausimą. Atidarykite failą šešioliktainiu režimu (hex view) ir perskaitykite baitus po paskutinio startxref; hibridinis failas rodo trumpą klasikinę dalį, kurios trailer žodyne yra /XRefStm. Or compare the object count a full parse reports against the highest object number that /Size declares in the trailer. A large gap means objects are hiding in streams the loader has not opened, which is the same shortfall that turns into a save-time failure later.
Įrašymo programos šios istorijos pusė, kaip apskritai kuriami objektų srautai ir suspaustos kryžminės nuorodos, yra aprašyta mūsų straipsnyje apie objektų srautus ir papildomus atnaujinimus. Kai hibridinis failas yra labai didelis, įkėlimo metodai, aprašyti Direct File API apžvalgoje dideliems PDF procesams, leidžia jį patikrinti neįkeliant viso failo į atmintį. Abu šie būdai puikiai dera su čia aprašytu atkūrimu, kuris yra platinamas kaip HotPDF Component dalis, skirta Delphi ir C++Builder, kartu su įkėlimo, redagavimo, šifravimo ir pasirašymo API, aprašytais kitose šio tinklaraščio dalyse.