Techninis straipsnis

Hibridinių nuorodų PDF failų iš „Word“ ir „Excel“ įkėlimas naudojant „Delphi“

Atidarykite PDF failą, kurį sukūrė „Microsoft Word“ ar „Excel“, perverskite jį 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 darbas nutrūks su EListError, arba išvestis atsidarys su pažeistos kryžminės nuorodos įspėjimu. Failas niekada nebuvo sugadintas. Tai hibridinių nuorodų failas, ir ta pati struktūra, kuri leidžia penkiolikos metų senumo peržiūros programai jį atidaryti, yra ta struktūra, kuri nugali įkėlėją, nustojantį skaityti per anksti

Tai vienas iš labiausiai paplitusių būdų, kaip PDF konvejeris, išlaikęs visus vidinius testus, susiduria su failu, kurio jis negali apdoroti pirmyn ir atgal. Visi įvesties duomenys buvo sugeneruoti vietoje, todėl jie niekada nebuvo hibridiniai. Pirmasis hibridinis failas atkeliauja 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 PDF 1.5 funkcijų, tokių kaip objektų srautai, ir kartu leidžia PDF 1.4 skaitytuvui atidaryti failą, kryžminių nuorodų informaciją įrašo du kartus. Yra klasikinė kryžminių nuorodų lentelė, fiksuoto pločio ASCII eilutės, kuriomis baigdavosi kiekvienas PDF iki 1.4 versijos, ir yra kryžminių nuorodų srautas, kuris indeksuoja likusią dalį. Klasikinės skilties pabaigos faile (trailer) yra /XRefStm įrašas, kurio reikšmė yra to srauto baitų poslinkis

Darbo pasidalijimas yra apgalvotas. Objektai, kuriuos turi pasiekti senas skaitytuvas, tarp jų katalogas ir puslapių medis, yra adresuojami iš klasikinės lentelės. Objektai, kurie buvo sudėti į suspaustus objektų srautus, klasikinėje lentelėje yra pažymėti kaip laisvi, naudojant f tipo įrašą, todėl 1.4 skaitytuvas juos tiesiog praleidžia ir niekada neužkliūva už struktūros, kurios negali išanalizuoti. Jų tikrosios vietos egzistuoja tik kryžminių nuorodų sraute. Tokio failo parašas yra jo pabaiga: trumpa klasikinė skiltis, dažnai ne daugiau kaip xref, po kurios eina 0 0 poskilsnio antraštė, kurios pabaigos įrašas (trailer) nukreipia į /XRefStm, kur ir yra tikrieji atkūrimo duomenys

Kodėl teisingas puslapių skaičius nieko neįrodo

Kadangi katalogas ir puslapių medis yra pasiekiami iš klasikinės lentelės tyčia, įkėlėjas, skaitantis tik tą lentelę, randa /Root, pereina puslapių medį ir pateikia teisingą puslapių skaičių. Yra viskas, ko reikia senam skaitytuvui, todėl failas atrodo sveikas. Dingę objektai yra tie, kurie buvo supakuoti į objektų srautus: „AcroForm“ laukų žodynai, žymėto PDF (tagged-PDF) struktūros elementai, ilga eilė mažų žodynų, kurie niekada neturėjo būti matomi pasenusioje peržiūros programoje

Jūs nepastebite spragos tol, kol kažkas nepajudina tų objektų, o visiškas perrašymas juos visus paliečia. Dokumento perėjimas norint jį iš naujo užšifruoti arba perrašyti yra būtent ta operacija, kuri prašo kiekvieno objekto numerio iš eilės, todėl simptomas iškyla išsaugojimo, o ne įkėlimo metu, toli nuo jo priežasties

Spąstai yra detektorius, kuris pamato xref ir sustoja

Pigus būdas nuspręsti, kaip failas indeksuojamas, yra sekti startxref ir ištirti pirmuosius baitus, į kuriuos jis nukreipia. Raktažodis xref reiškia klasikinę lentelę; srauto objektas reiškia kryžminių nuorodų srautą. Šis testas yra teisingas bet kokiam failui, kuris laikosi vienos schemos. Jis neteisingas hibridiniam failui, kurio startxref nukreipia į klasikinę skiltį vieninteliu tikslu – patenkinti senus skaitytuvus, tuo tarpu toje skiltyje esantis /XRefStm pabaigos įraše (trailer) yra vieta, kurioje iš tikrųjų indeksuojama didžioji dalis dokumento. Detektorius, kuris grąžina „klasikinis“ po pirmojo sutikto xref, niekada neperskaito /XRefStm, ir kiekvienas objektas, kuris egzistuoja tik sraute, tampa nematomas

var
  Pdf: THotPDF;
  PageCount: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    PageCount := Pdf.LoadFromFile('Invoice_XLS.pdf');  // skaičius yra teisingas
    // peržiūrėkite arba redaguokite įkeltą dokumentą čia
    Pdf.SaveLoadedDocument('Invoice_secured.pdf');     // pereina kiekvieną objektą
  finally
    Pdf.Free;
  end;
end;

Veikiant ankstyvo išėjimo detektoriui, įkėlimas atrodo gerai, ir būtent išsaugant iš naujo pasirodo trūkstami objektai. Sprendimas yra ne skaityti daugiau baitų pradžioje; tai hibridinio pabaigos įrašo (trailer) atpažinimas ir sekimas /XRefStm prieš nusprendžiant, kad failas baigtas

Sujungimo tvarka nediskutuojama

Kai abu indeksai perskaitomi, 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 yra nedidelė apgaulė pačioje formato esmėje. Hibridinis failas klasikinėje lentelėje pažymi savo suspaustus objektus kaip laisvus, kad seni skaitytuvai juos ignoruotų. Įkėlėjas, kuris laikosi politikos „pirmas pamatytas laimi“ ir pirmiausia perskaito klasikinę lentelę, įrašys tuos objektų numerius kaip laisvus, o vėliau atmes srauto įrašus, kurie iš tikrųjų juos suranda, nes vietos jau užimtos. Pakeiskite tvarką ir 2 tipo įrašai iš srauto, kurių kiekvienas yra objekto-srauto numeris plius indeksas, laimės tas vietas, kurios jiems priklauso, o klasikiniai įrašai išsidėstys aplink juos

Ta pati disciplina apsaugo nuo senesnės versijos ištrinto objekto atgaivinimo. Inkrementiniai atnaujinimai jungiasi atgal per /Prev, o 0 tipo laisvas įrašas yra sargas, nurodantis, kad naujesnė skiltis pašalino objekto numerį. Vėlesnei, senesnei grandinės skilčiai neturi būti leidžiama perrašyti to sargo su pasenusia vieta. Laikykite pirmą pamatytą autoritetu laisviems žymekliams ir ištrintas objektas liks ištrintas; elkitės su tuo nerūpestingai, ir failo paties istorija atgaivins turinį, kurį naujausia versija pašalino

Ką tai reiškia naudojant „HotPDF“

Variklis išsprendžia hibridinių nuorodų failų problemą už jus, ir jis tai daro kiekviename kelyje, kuris turi išanalizuoti kryžminių nuorodų duomenis. Įkelkite dokumentą su LoadFromFile arba LoadFromStream, atlikite pakeitimus ir iškvieskite SaveLoadedDocument; arba paleiskite vienkartinę operaciją, pvz., EncryptFile, kuri nuskaito įvestį ir įrašo išvestį. Bet kuriuo atveju atkūrimas nuskaito /XRefStm, sujungia srauto skiltį prieš klasikinius įrašus ir išsprendžia objektus, esančius srautuose, prieš tai, kai rašymas juos išvardija. AES-256 šifravimo kelias yra ten, kur problema pirmiausia pasirodė, nes dokumento šifravimas perrašo kiekvieną objektą ir todėl reikalauja, kad kiekvienas objektas jau būtų surastas

// Vienkartinė: nuskaitykite hibridinę įvestį, įrašykite AES-256 užšifruotą kopiją
Pdf.EncryptFile('Letter_DOC.pdf', 'Letter_secured.pdf',
  'owner-secret', '', aes256, [prPrint, prFillAnnotations]);

Detalė, kurią verta atsiminti, yra prieš API. Failai, gauti iš „Word“, „Excel“, „PowerPoint“ ir ilgo „Išsaugoti kaip PDF“ konvejerių sąrašo, paprastai yra hibridiniai, todėl įkėlėjas, kurį bandote tik su savo generatoriaus išvestimi, gali niekada tokio nesutikti testavimo metu. Papildykite savo testavimo duomenis dokumentais, eksportuotais iš tikrų „Office“ programų, o ne tik su jūsų paties kodo sugeneruotais failais

Kaip patikrinti įtariamą failą

Du patikrinimai greitai išsprendžia klausimą. Atidarykite failą šešioliktainiu rodiniu ir perskaitykite baitus po paskutinio startxref; hibridinis failas rodo trumpą klasikinę skiltį, kurios pabaigos žodyne yra /XRefStm. Arba palyginkite objektų skaičių, kurį pateikia pilna analizė, su didžiausiu objekto numeriu, kurį trailer skiltyje deklaruoja /Size. Didelis skirtumas reiškia, kad objektai slepiasi srautuose, kurių įkėlėjas neatidarė, o tai yra tas pats trūkumas, kuris vėliau virsta gedimu išsaugant

Tipiško „Excel“ eksporto pabaiga paverčia pirmąjį patikrinimą konkrečiu. Viskas po paskutinio xref raktažodžio yra paprastas ASCII, todėl parašas skaitomas tiesiai iš šešioliktainio rodinio (poslinkiai iliustraciniai, komentarai pridėti)

xref
0 0                          % tuščias klasikinis poskirsnis: visai nėra eilučių
trailer
<< /Size 216                 % vienu didesnis už didžiausią naudojamą objekto numerį
   /Root 1 0 R
   /Info 15 0 R
   /ID [<5C9A...> <5C9A...>]
   /XRefStm 87325            % kryžminių nuorodų srauto baitų poslinkis
>>
startxref
88710                        % nukreipia į aukščiau esančią klasikinę skiltį
%%EOF

0 0 poskirsnis yra ženklas: klasikinė lentelė su nuliu įrašų egzistuoja tik tam, kad neštų pabaigos įrašą (trailer), o jis egzistuoja daugiausia tam, kad pasakytų /XRefStm 87325. Detektorius, kuris sustoja ties xref raktažodžiu, šiuo metu yra matęs nieko neindeksuojantį indeksą. Kai verčiau skriptuojate patikrinimą nei vertinate iš akies, žymeklis visada yra paskutiniuose keliuose failo kilobaituose, todėl pakanka riboto skaitymo atgal

// Grąžina /XRefStm poslinkį nuo failo pabaigos, arba -1, jei
// žymeklio nėra (failas nėra hibridinis, arba tai išvis ne PDF)
function FindXRefStm(const FileName: string): Int64;
var
  FS: TFileStream;
  Tail: AnsiString;
  Len, P: Integer;
begin
  Result := -1;
  FS := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
  try
    Len := 2048;                        // pabaigos įrašas (trailer) yra pačioje pabaigoje
    if FS.Size < Len then
      Len := Integer(FS.Size);
    FS.Position := FS.Size - Len;       // ribotas skaitymas atgal: maks. 2 KB
    SetLength(Tail, Len);
    FS.ReadBuffer(Tail[1], Len);
  finally
    FS.Free;
  end;
  P := Pos(AnsiString('/XRefStm'), Tail);
  if P = 0 then
    Exit;                               // pabaigoje nėra hibridinio žymeklio
  Inc(P, Length('/XRefStm'));
  while (P <= Len) and (Tail[P] in [' ', #9, #13, #10]) do
    Inc(P);                             // praleisti tarpus po rakto
  Result := 0;
  while (P <= Len) and (Tail[P] in ['0'..'9']) do
  begin
    Result := Result * 10 + Ord(Tail[P]) - Ord('0');
    Inc(P);
  end;
end;

// Naudojimas: neneigiamas rezultatas nurodo baitą, nuo kurio prasideda srautas
if FindXRefStm('Invoice_XLS.pdf') >= 0 then
  Writeln('hibridinių nuorodų failas: perrašant reikės /XRefStm skilties');

Laikykite šį tyrimą pradiniu rūšiavimu, o ne analizatoriumi: jis pasako, kurie failai pakete nusipelno dėmesio prieš paleidžiant pakartotinio išsaugojimo užduotį, ir nieko daugiau. Ką įkėlėjas tada turi daryti su rastu poslinkiu, sekdamas skilčių grandinę, sujungdamas srauto įrašus prieš klasikinius, gerbdamas laisvų įrašų sargus, žingsnis po žingsnio paaiškinta mūsų susijusiame straipsnyje apie hibridinių nuorodų PDF failų iš „Office“ programų apdorojimą

Rašymo pusė šioje istorijoje – kaip apskritai sukuriami objektų srautai ir suspaustos kryžminės nuorodos – aprašyta mūsų straipsnyje apie objektų srautus ir inkrementinius atnaujinimus. Kai aptariamas hibridinis failas taip pat yra labai didelis, įkėlimo metodai Tiesioginio failų API vadove, skirtame didelėms PDF darbo eigoms leidžia jį patikrinti neskaitant viso jo į atmintį. Abu šie dalykai natūraliai dera su čia aprašytu atkūrimu, kuris tiekiamas kaip HotPDF komponento, skirto „Delphi“ ir „C++Builder“, dalis kartu su įkėlimo, redagavimo, šifravimo ir pasirašymo API, aptartais kitur šiame tinklaraštyje