Techninis straipsnis

Hibridinių nuorodų PDF failų iš „Office“ programų apdorojimas „Delphi“ aplinkoje

Eksportuokite dokumentą iš „Microsoft Word“ ar „Excel“ pasirinkę „Save as PDF“, ir dažniausiai diske esantis failas bus hibridinių nuorodų failas. Jame kryžminių nuorodų informacija pateikiama du kartus: vieną kartą kaip klasikinė fiksuoto pločio lentelė, kuria baigiamas kiekvienas PDF failas iki 1.4 versijos, ir vieną kartą kaip suspaustas kryžminių nuorodų srautas, nuo kurio iš tikrųjų priklauso didžioji dalis dokumento. Vienas baigiamasis raktas (angl. trailer key) /XRefStm sujungia abu rodinius, ir tai, ar įrankis mato visą dokumentą, priklauso nuo to, ar jis atsižvelgia į šį raktą

Šiame straipsnyje hibridiniai failai nagrinėjami iš vartotojo pusės: kaip atrodo baitai failo pabaigoje, kaip abu rodiniai išsiskiria redaguojant ir kaip „Delphi“ konvejeris gali aptikti ir nukreipti hibridinius įvesties duomenis. Kaip įkėliklis (angl. loader) sujungia rodinius ir kodėl jų eilės tvarka nėra keičiama, nagrinėjama mūsų „HotPDF“ straipsnyje apie hibridinių nuorodų failų įkėlimą; šis straipsnis skirtas visų pirma paties išdėstymo atpažinimui

Kodėl „Office“ eksportas įrašo indeksą du kartus

PDF 1.5 pristatė dvi funkcijas, kurios pakeitė failo struktūrą: kryžminių nuorodų srautus (angl. cross-reference streams), kurie saugo objektų indeksą kaip suspaustus dvejetainius duomenis, o ne paprasto teksto lentelę, ir objektų srautus (angl. object streams), kurie supakuoja daug mažų objektų į vieną „Flate“ suspaustą konteinerį. Rašyklė, kuri juos naudoja, sukuria mažesnius failus, tačiau PDF 1.4 skaitytuvas negali atidaryti rezultato, nes struktūrų, kuriomis jis remiasi, t. y. xref raktažodžio ir trailer žodyno, nebėra

ISO 32000-1 §7.5.8.4 apibrėžia kompromisą. Hibridinių nuorodų failas įrašo abu dalykus: klasikinę kryžminių nuorodų lentelę, nurodančią objektus, kuriuos turi pasiekti senasis skaitytuvas, tarp jų katalogą ir puslapių medį, ir kryžminių nuorodų srautą, kuris indeksuoja visa kita. Objektai, sudėti į objektų srautus, klasikinėje lentelėje pažymėti kaip laisvi (free), todėl 1.4 skaitytuvas juos praleidžia be jokių skundų; tikrosios jų vietos egzistuoja tik sraute. Tuomet klasikinis baigiamasis blokas (trailer) turi /XRefStm raktą, kuriame saugomas to srauto baitų poslinkis. Senas peržiūros įrankis niekada nenuskaito šio rakto ir atvaizduoja failą iš lentelės rodinio. Šiuolaikinis peržiūros įrankis juo vadovaujasi ir mato visą dokumentą. „Word“ ir „Excel“ būtent tokį išdėstymą naudoja jau daugelį metų, todėl hibridiniai failai nėra egzotiškas kraštutinis atvejis, o sudaro didelę dalį to, ką gauna verslo konvejeriai

Kaip atrodo hibridinio failo uodega

Išdėstymą lengviausia suprasti iš baitų. Štai nedidelio hibridinio failo pabaiga (uodega) su sutrumpintais poslinkiais; tikrame „Office“ eksporte /XRefStm reikšmė paprastai yra didelis poslinkis netoli failo pabaigos. Skaitymo tvarka yra ėjimas nuo uodegos, aprašytas mūsų PDF failų struktūros apžvalgoje: randamas %%EOF, nuskaitomas startxref, peršokama į lentelę

% ... body objects, including object streams and, at byte 116,
% the cross-reference stream (a stream object with /Type /XRef) ...

xref                    % classic section: what startxref points at
0 4
0000000000 65535 f      % slot 0: head of the free list, always present
0000000017 00000 n      % object 1: the catalog, visible to any reader
0000000000 65535 f      % object 2: marked free -- lives in an object stream
0000000000 65535 f      % object 3: same; only the stream view locates it
trailer
<<
  /Size 4
  /Root 1 0 R
  /XRefStm 116          % byte offset of the cross-reference stream
>>
startxref
7164                    % byte offset of the 'xref' keyword above
%%EOF

Šioje išklotinėje yra dvi detalės, atspindinčios visą mechanizmą. Pirma, startxref specialiai nurodo į klasikinę sekciją: tai yra adresas, į kurį turi patekti senas skaitytuvas. Kryžminių nuorodų srautas pasiekiamas tik per /XRefStm raktą trailer žodyne, todėl analizatorius, kuris niekada neieško šio rakto, niekada nesužino apie srauto egzistavimą. Antra, objektai 2 ir 3 yra gerybinio pobūdžio melas. Klasikinė lentelė skelbia juos laisvais, bet jie yra tikri objektai, esantys suspaustame konteineryje; laisvumo žyma apsaugo 1.4 skaitytuvą nuo užkliuvimo už įrašų, kuriais jis negali pasinaudoti. Vartotojas, pasikliaunantis vien klasikiniu rodiniu, padaro išvadą, kad didžioji šio dokumento dalis neegzistuoja

Kaip du rodiniai išsiskiria

Hibridinis failas, ką tik išsaugotas iš „Word“, viduje yra nuoseklus: abu rodiniai apibūdina tą patį dokumentą, kiekvienas savo deklaruotoje aprėptyje. Bėdos prasideda, kai failą redaguoja įrankis, suprantantis tik vieną iš rodinių. Apsvarstykite štampavimo įrankį, kuris prideda klasikinio stiliaus inkrementinį atnaujinimą: naujus objektus, naują xref sekciją, /Prev grandinę į ankstesnę sekciją ir naują baigiamąjį bloką. Jei tas baigiamasis blokas praranda /XRefStm raktą, srauto rodinys lieka našlaičiu; jei jis nukopijuoja seną reikšmę į priekį, srauto rodinys vis tiek apibūdina dokumentą tokį, koks jis buvo prieš redagavimą. Bet kuriuo atveju dabar du indeksai nesutampa dėl to, kas yra faile

Gautas failas turi išskirtinį gedimo požymį: viename rodinyje matomi objektai kitame yra dingę arba pasenę. Skaitytuvas, kuris sprendžia per srauto rodinį, randa atnaujinto objekto priešredagacinę versiją, arba apskritai neranda įrašo apie pridėtą objektą. Skaitytuvas per lentelės rodinį mato redagavimą, bet praranda suspaustų objektų, kuriuos randa tik srautas, pėdsakus. Praktiškai tai pasireiškia kaip formos laukai, kurie išlieka vienoje peržiūros programoje ir išnyksta kitoje, anotacijos, kurias, regis, ištrynė štampavimo operacija, arba paieškos, kurios nukreipia į visiškai netinkamą objektą

Dėl ko šiuos failus brangu derinti, tai kad „Adobe Acrobat“ paprastai juos atidaro be skundų: kai indeksas nesutampa su baitais, programa tyliai perkonstruoja kryžminių nuorodų duomenis ieškodama objektų antraščių, todėl sugadinto failo kūrėjas nemato nieko blogo. Gedimas išryškėja vėliau, kai failas pasiekia griežtą vartotoją – paruošimo spaudai validatorių (preflight), pasirašymo paslaugą, archyvavimo užduotį – kuris pasikliauja deklaruota struktūra ir praneša apie trūkstamus objektus arba kryžminių nuorodų neatitikimą. „Acrobat programoje atsidaro puikiai“ – taip prasideda beveik kiekvienas hibridinės desinchronizacijos pranešimas apie klaidą

Hibridinio failo aptikimas naudojant paprastą „Delphi“

Įvesčių klasifikavimui nereikia PDF bibliotekos. /XRefStm raktas gali būti tik klasikiniame trailer žodyne, o aktyvus baigiamasis blokas yra per kelis paskutinius failo kilobaitus, nes specifikacija reikalauja, kad %%EOF pasirodytų netoli fizinės pabaigos. Norint atlikti rūšiavimą (triažą), pakanka perskaityti apribotą uodegos langą ir jame atlikti paiešką:

uses
  System.SysUtils, System.Classes, System.StrUtils, System.Math;

function IsHybridReferencePdf(const FileName: string): Boolean;
const
  TailWindow = 2048;
var
  Stream: TFileStream;
  Buf: TBytes;
  Tail: string;
  Len, TrailerPos, NextPos, KeyPos, StartXrefPos: Integer;
begin
  Result := False;
  Stream := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
  try
    if Stream.Size < 48 then
      Exit;
    Len := Min(TailWindow, Integer(Stream.Size));
    SetLength(Buf, Len);
    Stream.Position := Stream.Size - Len;
    Stream.ReadBuffer(Buf[0], Len);
  finally
    Stream.Free;
  end;

  // Every keyword involved is 7-bit ASCII, so a byte-wise decode is safe
  Tail := TEncoding.ANSI.GetString(Buf);

  // Find the LAST 'trailer' keyword: with incremental updates,
  // the newest trailer is the one that governs the file
  TrailerPos := 0;
  NextPos := Pos('trailer', Tail);
  while NextPos > 0 do
  begin
    TrailerPos := NextPos;
    NextPos := PosEx('trailer', Tail, NextPos + 1);
  end;
  if TrailerPos = 0 then
    Exit;  // no classic trailer: a pure xref-stream file, not hybrid

  // A hybrid trailer carries /XRefStm between 'trailer' and 'startxref'
  KeyPos := PosEx('/XRefStm', Tail, TrailerPos);
  StartXrefPos := PosEx('startxref', Tail, TrailerPos);
  Result := (KeyPos > 0) and
    ((StartXrefPos = 0) or (KeyPos < StartXrefPos));
end;

Trys rezultatai atitinka tris išdėstymus. Tik klasikiniame faile yra baigiamasis blokas, bet nėra /XRefStm: grąžinama False. Faile, kuris visiškai naudoja kryžminių nuorodų srautus, apskritai nėra trailer raktažodžio, jo baigiamojo bloko raktai yra srauto žodyne: taip pat False (teisingai, nes toks failas yra suspaustas, o ne hibridinis). Tik dvigubo indeksavimo išdėstymas grąžina True

Gamybiniam naudojimui verta pridėti dvi papildomas apsaugos eilutes. Išanalizuokite sveikąjį skaičių po /XRefStm, pereikite į tą poslinkį ir patvirtinkite, kad ten iš tikrųjų yra srauto objektas su /Type /XRef; sutrumpintame faile gali likti raktas, o srautas dingti, o tai priklauso kitai kategorijai nei sveikas hibridas. Taip pat lango dydį traktuokite kaip parametrą: 2 KB apima įprastą „Office“ išvestį, tačiau neįprastai didelis baigiamasis žodynas gali išstumti raktažodį už ribų, o praplėsti langą yra geriau nei netyčia paskelbti failą klasikiniu

Hibridinių failų nukreipimas per „Delphi“ konvejerį

Aptikimas leidžia priimti sprendimą dėl nukreipimo. Failams, kurie yra tik skaitomi, atvaizduojami ar validuojami, naudokite įkėliklį, kuris išsprendžia abu rodinius, ir tada tikrinkite elgseną, o ne baitus. „PDFium“ komponentas įkėlimo metu išanalizuoja /XRefStm grandinę, todėl jūsų kodo matoma objektų lentelė yra sujungta, ir mūsų straipsnyje apie objektų ir kryžminių nuorodų srautų validavimą aprašyti patikrinimai taikomi nepakeisti. Jei desinchronizuotas hibridas pažeistas taip stipriai, kad jo nepavyksta įkelti, variklis apie tai praneša per savo klaidų rinkinį: FPDF_ERR_SUCCESS, FPDF_ERR_UNKNOWN, FPDF_ERR_FILE, FPDF_ERR_FORMAT, FPDF_ERR_PASSWORD, FPDF_ERR_SECURITY ir FPDF_ERR_PAGE, kur FPDF_ERR_FORMAT parodo struktūrinį pažeidimą. Tačiau nesikliaukite vien šiuo signalu: „PDFium“ pagal dizainą yra atlaidus ir tyliai perkonstruoja daugelį nenuoseklių failų, todėl sėkmingas įkėlimas įrodo, kad failą buvo galima atkurti, o ne tai, kad jo du rodiniai sutampa. Prasmingas nuoseklumo patikrinimas yra palyginti tai, ką randa pilnas objektų perėjimas, su tuo, ką deklaruoja baigiamojo bloko /Size

Failams, kuriuos jūsų konvejeris modifikuoja, saugiausia strategija yra apskritai neleisti jiems likti hibridiniais. Įkėlus ir po to visiškai išsaugojus per „HotPDF“, dokumentas perrašomas su viena nuoseklia kryžmine nuoroda vienoje formoje: jokio /XRefStm, jokio antro rodinio, kuris galėtų prarasti sinchronizaciją, kiekvienas objektas priklauso lygiai vienam indekso įrašui. Toks normalizavimas yra tai, ko norite prieš įtraukiant į archyvą, prieš griežtą tolesnį RIP ar pasirašymo paslaugą, ir po bet kokio redagavimo, pritaikyto hibridiniam įvesties failui. Tai veikia, nes įkėliklis teisingai sujungė rodinius juos įkeliant – šis mechanizmas išsamiai aprašytas „HotPDF“ hibridinių nuorodų straipsnyje

Vienintelė failų klasė, kurią reikėtų palikti ramybėje, yra skaitmeniniu būdu pasirašyti dokumentai. Pilnas perrašymas perkelia kiekvieną baitą, o tai panaikina bet kokį parašą, apskaičiuotą originaliuose diapazonuose. Pasirašyto hibrido pakeitimas turi būti atliekamas kaip tinkamas inkrementinis atnaujinimas, išlaikantis abu rodinius; failas, kurį reikia tik nuskaityti, turėtų būti perduodamas nepaliestas. Normalizavimas skirtas jūsų valdomiems failams; pasirašytus failus jūs tik papildote

Hibridinių nuorodų PDF failai nėra neteisingai suformuoti; tai paties formato suderinamumo tiltas, ir „Office“ programos toliau juos gamins, kol diegimo bazėse išliks PDF 1.4 skaitytuvai. Konvejeris, galintis pastebėti /XRefStm raktą, validuoti sujungtą dokumentą su „PDFium“ komponentu ir atkurti švarią vieno indekso išvestį su „HotPDF“ komponentu, elgiasi su jais taip, kas jie iš tikrųjų yra: paprastos įvestys su viena papildoma gaire baigiamajame bloke