Tehnični članak

Nalaganje hibridno-referenčnih datotek PDF iz Worda in Excela v Delphiju

Odprite PDF, ki sta ga izdelala Microsoft Word ali Excel, prelistajte po njem in nič ne bo videti nenavadno. Naložite ga v program Delphi, preberite nazaj število strani in to število bo pravilno. Nato ga ponovno shranite z vklopljenim šifriranjem in opravilo (job) ne uspe z napako EListError ali pa se izhod odpre z opozorilom o poškodovanem navzkrižnem sklicu (damaged cross-reference warning). Datoteka ni bila nikoli poškodovana. To je hibridno-referenčna datoteka (hybrid-reference file) in natanko ista struktura, ki petnajst let staremu pregledovalniku omogoča, da jo odpre, je tista struktura, ki premaga nalagalnik (loader), ki prehitro neha brati

To je eden najpogostejših načinov, kako cevovod PDF (PDF pipeline), ki je opravil vsak notranji test, sreča datoteko, ki je ne more odpreti in zapreti (round-trip). Vsi vhodi (inputs) so bili ustvarjeni interno (in-house), zato nikoli niso bili hibridni. Prva hibridna datoteka prispe na dan, ko stranka posreduje račun (invoice), izvožen iz preglednice (spreadsheet)

Kaj dejansko zapišeta Word in Excel

ISO 32000-1 opisuje hibridno referenčno postavitev (hybrid-reference layout) v odstavku §7.5.8.4. Aplikacija, ki želi funkcionalnosti (features) različice PDF 1.5, na primer objektne tokove (object streams), medtem ko hkrati bralniku PDF 1.4 še vedno dopušča odpiranje datoteke, zapiše informacije za navzkrižni sklic dvakrat. Obstaja klasična tabela z navzkrižnimi sklici (classic cross-reference table), vrstice v fiksni širini v standardu ASCII, s katero so se končali vsi dokumenti PDF do različice 1.4, in obstaja tok za navzkrižni sklic (cross-reference stream), ki indeksira ostalo. Priklopnik (trailer) klasične sekcije (classic section) nosi vnos /XRefStm, katerega vrednost je odmik v bajtih (byte offset) tega toka

Delitev dela je namerna. Objekti, ki jih stari bralnik mora doseči, med njimi sta katalog (catalog) in drevo strani (page tree), so naslovljivi (addressable) iz klasične tabele. Objekti, ki so bili zviti (folded) v stisnjene objektne tokove, so v klasični tabeli označeni kot prosti (free) z vnosom tipa f, zato jih bralnik različice 1.4 preprosto preskoči in se ne spotakne ob strukturo, ki je ne zna razčleniti (parse). Njihove prave lokacije so zgolj v toku z navzkrižnimi sklici (cross-reference stream). Značilnost takšne datoteke je njen rep: kratka klasična sekcija, pogosto nič drugega kot xref, ki mu sledi glava podsekcije (subsection header) 0 0, katere priklopnik kaže na /XRefStm, kjer so shranjeni dejanski podatki za obnovitev (recovery data)

Zakaj pravilno število strani ne dokazuje ničesar

Ker sta katalog in drevo strani nalašč dosegljiva iz klasične tabele, nalagalnik (loader), ki prebere zgolj to tabelo, najde /Root, se sprehodi po drevesu strani in sporoči pravo število strani. Prisotno je vse, kar potrebuje stari bralnik, zato je datoteka videti zdrava. Objekti, ki so manjkali, so tisti, pakirani v objektne tokove (object streams): slovarji s polji AcroForm, strukturni elementi s tagi v PDF-ju in dolg rep majhnih slovarjev, ki jim (starejšemu) bralniku nikoli ni bilo treba biti vidni

Vrzeli (gap) ne opazite, dokler se nekaj ne dotakne teh objektov, in polno ponovno shranjevanje (full resave) se dotakne vseh. Prehod čez celoten dokument zaradi njegovega ponovnega šifriranja ali ponovnega pisanja (rewrite) je natanko tista operacija, ki zahteva vsako številko objekta po vrsti, in to je razlog, da simptom pride na plano med postopkom shranjevanja in ne med postopkom nalaganja, daleč od njegovega vzroka

Past je detektor, ki vidi xref in se ustavi

Poceni način odločanja o tem, kako je datoteka indeksirana, je ta, da se sledi parametru startxref in se preveri prve bajte, na katere pokaže. Ključna beseda xref pomeni klasično tabelo; objekt vrste stream pomeni tok (stream) za navzkrižne sklice. Ta test je pravilen za vsako datoteko, ki se obveže eni shemi (scheme). Napačen pa je za hibridno datoteko, katere startxref meri na klasično sekcijo z edinim namenom zadostitve zahtev starih bralnikov, pri tem pa je tisti /XRefStm v priklopniku (trailer) tiste sekcije tisti, kjer je v resnici indeksiran večji del dokumenta. Detektor, ki ob prvem xref, s katerim se sreča, vrne "classic", nikoli ne prebere /XRefStm, in s tem vsak objekt, ki živi le v tistem toku (stream), postane neviden

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;

Z vgrajenim detektorjem za predčasni izstop (early-exit detector) deluje (load) v redu, pri ponovnem shranjevanju (resave) pa se oglasijo odsotni objekti (absent objects). Popravek ni v tem, da bi na začetku prebrali več bajtov; je v tem, da prepoznamo hibridni trailer in sledimo zapisu /XRefStm, preden odločimo, da je datoteka končana

Vrstni red spajanja se ne da pogajati (Merge order is not negotiable)

Ko sta bila prebrana oba indeksa, se lahko združita samo v eno smer. Tabela za navzkrižne sklice (cross-reference stream) mora biti spojena (merged) prva in klasični vnosi vstavljeni okoli nje. Razlog je majhna prevara (deception) v srcu tistega formata. Hibridna datoteka označi svoje stisnjene objekte (compressed objects) v klasični tabeli kot proste (free), tako da jih stari bralniki (old readers) prezrejo. Nalagalnik, ki spoštuje pravilo, da zmaga tisto, kar vidi najprej (first-seen-wins policy) in najprej prebere klasično tabelo (classic table), bo tiste številke objektov (object numbers) zabeležil kot proste (free), nato pa zavrgel tiste vnose iz toka, s pomočjo katerih bi se dejansko določila njihova lokacija (locate), ker so ta mesta že zasedena (slots are already taken). V nasprotnem vrstnem redu si tisti vnosi tipa 2 iz toka, kjer sta številka toka za objekt plus in indeks vsak zase, izborijo mesto v vnosih, ki jim pripada, klasični vnosi (classic entries) pa so postavljeni okoli njih

Ista disciplina varuje pred starejšo revizijo, ki vstavi izbrisan objekt iz mrtvih (resurrecting a deleted object). Inkrementalne posodobitve (incremental updates) se povežejo (chain backward) prek /Prev in prost vnos (free entry) vrste (type) 0 v verigi označuje, da je neka nedavna sekcija vrnila staro številko objekta, preden postane spet uporaben. Poznejšemu, starejšemu delu v veriženju ne sme biti omogočeno zamenjati tistega varuha z zarošenostjo in sušo starih izzivov ali staro postavitvijo (stale location). Če ravnamo po tistem najprej videnem (first-seen), je to merodajno in vnos bo avtoriziran za varuhe prostih (free marker) oznak in s tem za vrnitev, pa tudi tisti izbrisan obdrži svojo lastnost; če preidemo to varovanje neprevidno, zgodovina lastne datoteke reanimira vsebino, ki jo je izbrisala zadnja revizija

Kaj to pomeni v HotPDF

Pogon (engine) za vas razreši hibridno-referenčne datoteke (hybrid-reference files) in stori to na vsaki poti, ki mora razčleniti (parse) podatke za navzkrižni sklic (cross-reference data). Naložite (load) dokument z ukazom LoadFromFile ali LoadFromStream, naredite svoje spremembe (changes) in kličite SaveLoadedDocument; ali zaženite enkratno operacijo (one-shot operation), kakršna je EncryptFile, ki prebere vnos (input) in zapiše izhod (output). Na ta ali oni način obnovitev prebere ukaz /XRefStm, spoji (merges) sekcijo za stream pred klasičnimi vnosi (classic entries) in razreši objekte (objects), ki preživijo v tokovih pred navedbo prikaza ob vpisu ali shranjevanju. Šifrirna (encryption) pot tipa AES-256 je tista točka, kjer se je težava najprej razkrila, saj zahteva kodiranje, s tem pa pisanje prek vsakega dokumenta oziroma predmeta ali prepisa samega in preden ga najde

// 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]);

Podrobnost (detail), ki jo je vredno odnesti s sabo, sedi v toku višje (upstream) od vmesnika (API). Datoteke, ki prihajajo iz programov Word, Excel, PowerPoint in s precej dolgega seznama poti (pipelines) za "Shrani kot PDF" ("Save as PDF"), so rutinsko hibridne (hybrid), zato se nalagalnik, ki ga preizkušate samo in le z izhodi lastnega generatorja (generator output), na testiranju (in testing) morda ne bo nikoli srečal z nobeno od njih. Obogatite svoja testna orodja z dokumenti (documents), ki so bili izvoženi iz pravih aplikacij programa Office, ne le z datotekami, ki jih je proizvedla (produced) vaša lastna programska koda

Preverjanje datoteke, v katero sumite (Checking a file you suspect)

Dve preiskavi hitro odločita, za kaj gre v tem vprašanju. Odprite datoteko v heksadecimalnem (hex) prikazu in preberite bajte po zadnjem oznaki startxref; hibridna datoteka (hybrid file) prikazuje kratko klasično sekcijo, v kateri priklopni slovar vsebuje vnos /XRefStm. Lahko pa preštejete in primerjate izpis celotnega preleta s podatkom za najvišjo številko objekta, ki jo sporoči (declares) tisti parameter /Size znotraj tistega dela za klasično določanje tiste iste datoteke. V kolikor gre za večji rez ali gap v primerjavi, to pomeni, da je ostalo ogromno neobjavljenih tistih delcev ali objektov znotraj strukture s skritimi stream poteki dokumenta, medtem ko napaka prihrani z isto hibo in tistim fail shranjevanja tisti zadnji konec ali fazo, se pravi pomanjkanje enake smeri in razpotij ali prekinitev tistih nivojev pri prejemu ali napaki v kasnejšem shranjevanju

Rep datoteke in s tem tisti izvoz, ki bi ga po navadi doživeli pri izvozu prek orodja Excel in ki bo z dokazom tistega the-tail prišel kot rešitev ali pa določil prvi test za to: vsak tisti ključ pri in onkraj in pa tisti ostanek naprej po vrsti, to in naprej preteklost po vsakem novem final tistega prelomnega keyword pred koncem oz. naprednim finalom besede v obliki xref je samo plain ali ASCII format oziroma besedilo ali pa preprosto in plain, kjer na tisto temo postane signatura seveda berljiva navzdol ali pa se odpre in tisti ofset izpusti naprej kar do dodanih ali na novih vsebink, če ga prevajate vizualno in to v heksadecimalni vizualizaciji z bralno vizijo na vpogled z vpogledanimi komentarji pred potrditvijo (oznake, označbe ob primeru in anotacije dodane posebej; odmiki kot ofseti, s posameznimi obogatenimi z ofseti za ilustrativne namene)

xref
0 0                          % empty classic subsection: no rows at all
trailer
<< /Size 216                 % one past the highest object number in use
   /Root 1 0 R
   /Info 15 0 R
   /ID [<5C9A...> <5C9A...>]
   /XRefStm 87325            % byte offset of the cross-reference stream
>>
startxref
88710                        % points at the classic section above
%%EOF

Vmesni razdelek oziroma vnos s tisto podsekcijo 0 0 je razlog, po katerem vemo vse o ostalih pri uvodnem tistem začetku na vsak tak preizkus (tell): klasična tabela z navzkrižnimi sklici in na nič vnosov obstaja le iz teh razlogov pred njo izključno zaradi trailer navedbe tistega konca; tako seveda tudi zato, da napove in izrazi navedbo in določa samo z vsebino /XRefStm 87325. Detektor, ki neha ali pa se ustavi pri tistem xref pri navedenih z ključnimi besedami teh razlogov in teh preiskav na danem v tisti situaciji ali pa na primer, pri točki in pri tem tistem delu tistega vidnega in na pregledu ničesar preostanka (an index of nothing). V primeru se preprosto želite tiste napake, ko ga preverite samo še, kot si s script to tisto točko po ogledovanju pregledujete za to točno samo mesto tam po koncu samo preiskava z branjem (a bounded backward read is enough), to zadosti že in na tistem ali s takšnim na novo opisanem in samo preučenem delu proti tistim datotekam proti sami velikosti prebrane datoteke

// Returns the /XRefStm offset from the file's tail, or -1 if the
// marker is absent (the file is not hybrid, or not a PDF at all)
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;                        // the trailer lives in the tail
    if FS.Size < Len then
      Len := Integer(FS.Size);
    FS.Position := FS.Size - Len;       // bounded backward read: 2 KB max
    SetLength(Tail, Len);
    FS.ReadBuffer(Tail[1], Len);
  finally
    FS.Free;
  end;
  P := Pos(AnsiString('/XRefStm'), Tail);
  if P = 0 then
    Exit;                               // no hybrid marker in the tail
  Inc(P, Length('/XRefStm'));
  while (P <= Len) and (Tail[P] in [' ', #9, #13, #10]) do
    Inc(P);                             // skip whitespace after the key
  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;

// Usage: a non-negative result names the byte where the stream starts
if FindXRefStm('Invoice_XLS.pdf') >= 0 then
  Writeln('hybrid-reference file: resave will need the /XRefStm section');

Sondo obravnavajte kot triažo (triage), in ne kot razčlenjevalnik (parser): navaja ali pa vas pouči in informira katera datoteka v zbirki si zasluži pozornost na obravnavi ali ko ali preden se ponovno tisto opravilo navedbe shrani ob zagonu iz tistega zagona ali pri ponovnem pred navedbo, iz katerega tisto pride v ta opravila, in popolnoma ali preprosto in samo ne preveč. Nalagalnik pa bi moral ali pa je zadolžen (A loader must then do), ki nato uporablja (kaj nalagalnik stori ob ugotovitvi odklona preostalega ob tem istem offest preostanku), v kateri se veriga združuje z razdelki in tistimi tokovnimi (stream) oznakami, spoštovanjem praznih vnosov (free-entry sentinels), ki se vrstijo (merging ahead) klasično navedbo z zamenjavami the classic, na povezavi the section navedenem chain točno pred vso situacijo the classic ones in z razlago do posameznih, korak za korakom (step by step) obdeluje v našem spremljevalnem prispevku (companion article) glede rokovanja ali ravnanja s hibridno-referenčnimi (hybrid-reference PDFs from Office applications) zapisi v Delphiju

Stran pripovedovalca (The writer's side of this story) o tem in pa preostalo, kako na the same ob tistih first place na prvem mestu kako sploh v takem ali z enakim uspehom the first ali prvem v the prevem ali prvem predelu the object objekte, in the na enakem objektu ustvari the compressed na navzkrižni pridelani stisnjen predel pridelovalca, je napisano (covered in) v našem tistem prispevku (our article) o the objest tokovih na preverjanje objektov o the tisti na ali za inkrementalne objektne the na inkrementalni prenovi ali posodobitvah the ali za in o the in na incremental za update updates. Ko v primerih ali ko je v teh tistih the the z the na posamezen velik prerez pri hybrid the za the in za preglede na primer za the posamezno veliko v the ali pa hybrid velika pri primerih na tistih primerih tiste pri takrat the v file pri in za zelo veliko tisto z datotekah za v loading ali o in na v prenašanju navedb tistih v Navodila the the v poteku z ali za prikaze in the z za in tisti in na Direct File API API walkthrough for large PDF the veliki workflows za delovne datoteke the the workflows o the in z in preberite z in za in ali za preveri za memory the the whole na v and on spomin (in memory) the whole thing on ali the inspect reading. Oba poteka gresta on (Both pair naturally) on v on naravno naravnost z in proti z the recovery ki na s s tistimi ali na on tu v in ob in z tisto na in s tistim kot the kot or as z the z na to tistem the in na ki so in the v HotPDF z Component ob komponenti the HotPDF Component na in v na Delphi or C++Builder na za in iz okolja in za the za z s tistimi the v iz elsewhere poleg ali z navedenih covered and za the o tem za loading za s šifriranjem z the na in z blogu na ali za urejanjem the elsewhere v z s the elsewhere na v covered o covered s elsewhere covered