Tekninen artikkeli

Office-sovellusten hybridiviite-PDF-tiedostojen käsittely Delphissä

Kun viet (export) asiakirjan Microsoft Wordista tai Excelistä valitsemalla Tallenna PDF:nä (Save as PDF), levyllä oleva tiedosto on useammin kuin ei (more often than not) hybridiviitetiedosto (hybrid-reference file). Se kantaa ristiviittaustietonsa (cross-reference information) kahdesti: kerran klassisena kiinteälevyisenä (fixed-width) taulukkona, joka päätti jokaisen PDF-tiedoston versioon 1.4 asti, ja toisen kerran pakattuna ristiviittausvirtana (cross-reference stream), josta (which) suurin osa asiakirjasta todellisuudessa riippuu. Yksi ainoa trailer-avain (trailer key), /XRefStm, ompelee (stitches) nuo kaksi näkymää yhteen, ja se, näkeekö työkalu koko asiakirjan, riippuu siitä, seuraako (follows) se tuota avainta

Tässä artikkelissa tarkastellaan hybriditiedostoja (hybrid files) kuluttajan (consuming side) näkökulmasta: miltä tiedoston lopussa (tail) olevat tavut (bytes) näyttävät, miten kaksi näkymää (views) ajautuvat erilleen (drift apart) muokkauksen (editing) myötä, ja kuinka Delphi-putki (pipeline) voi tunnistaa ja reitittää (route) hybridisyötteitä (hybrid inputs). Se, miten lataaja (loader) yhdistää (merges) näkymät, ja miksi niiden järjestys ei ole neuvoteltavissa (not negotiable), on aiheena HotPDF-artikkelissamme hybridiviitetiedostojen lataamisesta; tässä artikkelissa on kyse asettelun (layout) tunnistamisesta ylipäätään (in the first place)

Miksi Office-viennit kirjoittavat indeksin kahdesti

PDF 1.5 esitteli kaksi ominaisuutta, jotka muuttivat tiedoston muotoa: ristiviittausvirrat (cross-reference streams), jotka tallentavat (store) objektihakemiston (object index) pakattuna binaaridata (compressed binary data) selkokielisen (plaintext) taulukon (table) sijaan, ja objektivirrat (object streams), jotka pakkaavat useita pieniä objekteja (objects) yhteen Flate-pakattuun (Flate-compressed) säiliöön (container). Kirjoittaja (writer), joka käyttää niitä, tuottaa pienempiä tiedostoja, mutta PDF 1.4 -lukija (reader) ei voi avata tulosta (result), koska rakenteet (structures), joihin se nojaa, xref-avainsana ja trailer-sanakirja (dictionary), ovat poissa

ISO 32000-1 §7.5.8.4 määrittelee kompromissin. Hybridiviitetiedosto (hybrid-reference file) kirjoittaa molemmat: klassisen ristiviittaustaulukon, joka osoittaa (addressing) objektit, jotka vanhan lukijan täytyy saavuttaa (reach) — muun muassa hakemiston (catalog) ja sivupuun (page tree) — sekä ristiviittausvirran, joka indeksoi kaiken muun. Objektit, jotka on taitettu (folded) objektivirtoihin, on merkitty (marked) vapaiksi (free) klassisessa taulukossa, joten 1.4-lukija ohittaa ne (skips them) valittamatta (without complaint); niiden todelliset sijainnit (real locations) ovat (exist) olemassa vain (only) virrassa. Klassinen traileri (classic trailer) sisältää sitten /XRefStm-avaimen, joka pitää sisällään tuon virran tavusiirtymän (byte offset). Vanha katseluohjelma (old viewer) ei koskaan lue avainta ja renderöi tiedoston taulukkonäkymän (table view) perusteella. Moderni katseluohjelma seuraa sitä ja näkee koko asiakirjan. Word ja Excel ovat tuottaneet (emitted) tismalleen tällaista asettelua jo vuosia, minkä vuoksi hybriditiedostot (hybrid files) eivät ole (are not) eksoottinen erikoistapaus (exotic corner case), vaan merkittävä osa (large share) siitä, mitä liiketoimintaputket (business pipelines) vastaanottavat

Miltä hybriditiedoston häntä (tail) näyttää

Asettelu on helpointa ymmärtää tavuista. Tässä on pienen hybriditiedoston häntä, siirtymiä lyhennettynä (offsets shortened); todellisessa Office-viennissä /XRefStm-arvo on tyypillisesti (typically) suuri (large) siirtymä lähellä tiedoston loppua. Lukujärjestys on hännästä aloittava (tail-first) läpikäynti (walk), joka on kuvattu PDF-tiedoston rakenteen yleiskatsauksessamme: etsi %%EOF, lue startxref, hyppää (jump) taulukkoon (table)

% ... runko-objektit (body objects), mukaan lukien objektivirrat ja, tavun 116 kohdalla,
% ristiviittausvirta (stream-objekti, jolla /Type /XRef) ...

xref                    % klassinen osio: mihin startxref osoittaa
0 4
0000000000 65535 f      % paikka 0: vapaiden (free) listan pää (head), aina (always) läsnä
0000000017 00000 n      % objekti 1: hakemisto (catalog), näkyvissä kaikille lukijoille
0000000000 65535 f      % objekti 2: merkitty vapaaksi -- elää objektivirrassa (object stream)
0000000000 65535 f      % objekti 3: sama; vain virtanäkymä (stream view) paikallistaa sen
trailer
<<
  /Size 4
  /Root 1 0 R
  /XRefStm 116          % ristiviittausvirran tavusiirtymä (byte offset)
>>
startxref
7164                    % yllä olevan 'xref'-avainsanan tavusiirtymä
%%EOF

Kaksi yksityiskohtaa tässä vedoksessa (dump) kantavat koko mekanismin. Ensinnäkin, startxref osoittaa tarkoituksella klassiseen osioon (classic section): tuo (that) on osoite, johon (where) vanhan lukijan täytyy laskeutua (land on). Ristiviittausvirta on saavutettavissa (reachable) vain trailer-sanakirjan (dictionary) sisällä olevan /XRefStm-avaimen kautta, joten jäsennin (parser), joka ei koskaan etsi tuota avainta, ei koskaan saa tietää virran (stream) olevan olemassa. Toiseksi, objektit 2 ja 3 ovat hyvänlaatuisia (benign) valheita (lies). Klassinen taulukko (classic table) julistaa (declares) ne vapaiksi (free), mutta ne ovat todellisia objekteja, jotka istuvat (sitting) pakatun säiliön (compressed container) sisällä; vapaa-merkintä estää (keeps from) 1.4-lukijaa (reader) kompastumasta (tripping over) merkintöihin (entries), joita se ei voi käyttää. Kuluttaja, joka luottaa pelkästään (alone) klassiseen näkymään (classic view), päättelee (concludes), että suurin osa (most) tästä asiakirjasta ei ole (does not) olemassa (exist)

Miten kaksi näkymää ajautuvat erilleen (drift apart)

Wordista tuoreeltaan (fresh out of) tullut hybriditiedosto on sisäisesti johdonmukainen (internally consistent): molemmat (both) näkymät (views) kuvaavat saman (same) asiakirjan (document), kumpikin oman julistetun soveltamisalansa (declared scope) sisällä. Ongelmat (The trouble) alkavat (starts), kun tiedostoa muokkaa (edited by) työkalu, joka (which) ymmärtää vain toisen (one of) näkymistä. Kuvittele (Consider) leimaustyökalu (stamping utility), joka liittää (appends) perään klassistyylisen (classic-style) inkrementaalisen päivityksen (incremental update): uusia objekteja, uuden xref-osion, /Prev-ketjun edelliseen osioon ja uuden trailerin. Jos tuo (that) traileri (trailer) pudottaa (drops) /XRefStm-avaimen, virtanäkymä orpoutuu (is orphaned); jos se kopioi vanhan (old) arvon eteenpäin, virtanäkymä kuvaa edelleen asiakirjaa sellaisena (as) kuin se oli ennen muokkausta (edit). Oli miten oli (Either way), kaksi indeksiä ovat nyt (now) eri mieltä (disagree) siitä, mitä tiedosto sisältää (contains)

Tuloksena syntyvällä (The resulting) tiedostolla (file) on (has a) tunnusomainen (distinctive) epäonnistumisen allekirjoitus (failure signature): yhdessä (one) näkymässä näkyvät (visible) objektit puuttuvat (are missing) tai ovat (are) vanhentuneita (stale) toisessa. Lukija, joka (which) selvittää (resolves through) virtanäkymän kautta (through), löytää (finds) päivitetyn objektin muokkausta edeltävän (pre-edit) version, tai (or) ei yhtään merkintää (no entry at all) perään liitetylle (appended) objektille (one). Taulukkonäkymää (table view) lukeva lukija (reader on) näkee muokkauksen (edit), mutta kadottaa jäljet (loses track of) pakatuista objekteista (compressed objects), jotka (the) pelkkä (alone) virta (stream) paikantaa (locates). Käytännössä (In practice) tämä (this) nousee pintaan (surfaces) lomakekenttinä (form fields), jotka (that) säilyvät (survive) yhdessä katseluohjelmassa (viewer) ja (and) katoavat (vanish) toisessa, huomautuksina (annotations), jotka leimauskierros (stamping pass) näyttää poistaneen (appears to have deleted), tai (or) hakuina (lookups), jotka laskeutuvat (land on) täysin väärään (wrong) objektiin (object entirely)

Näiden tiedostojen virheenjäljityksestä (debug) tekee kallista (expensive) se, että Adobe Acrobat avaa ne yleensä valittamatta: kun indeksi on eri mieltä tavujen (bytes) kanssa, se rakentaa (rebuilds) ristiviittaustiedot (cross-reference data) hiljaisesti uudelleen skannaamalla (scanning for) objektien otsikoita (object headers), joten rikkonaisen tiedoston (broken file) tuottaja ei näe mitään vikaa (wrong). Epäonnistuminen (failure) nousee pintaan (surfaces) myöhemmin, kun tiedosto saavuttaa (reaches a) tiukan kuluttajan, preflight-validaattorin (preflight validator), allekirjoituspalvelun (signing service), arkiston sisäänottoajon (archival ingest job), joka luottaa (trusts) julistettuun rakenteeseen (declared structure) ja (and) raportoi puuttuvista objekteista (missing objects) tai (or a) ristiviittauksen epäsuhdasta (cross-reference mismatch). "Se (It) avautuu (opens) ihan hyvin (fine) Acrobatissa" on se, miten lähes jokainen hybridin epäsynkronointitiketti (hybrid desynchronization ticket) alkaa

Hybriditiedoston (hybrid file) tunnistaminen (Detecting) puhtaassa (plain) Delphissä

Syötteiden (inputs) luokittelu (Classifying) ei (does not) vaadi PDF-kirjastoa (library). /XRefStm-avain voi (can) esiintyä vain klassisen (classic) trailer-sanakirjan (trailer dictionary) sisällä, ja (and) aktiivinen (active) traileri istuu (sits) tiedoston viimeisen parin kilotavun sisällä, koska spesifikaatio vaatii (requires), että %%EOF ilmestyy fyysisen lopun (physical end) lähelle (near). Rajatun hännän ikkunan (bounded tail window) lukeminen ja sen etsiminen (searching it) riittää seulontaan (triage):

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;

  // Jokainen (Every) osallistuva avainsana (keyword) on 7-bittinen ASCII, joten (so) tavuttainen purku (byte-wise decode) on turvallinen
  Tail := TEncoding.ANSI.GetString(Buf);

  // Etsi (Find) VIIMEINEN (LAST) 'trailer'-avainsana: inkrementaalisten päivitysten (incremental updates) kanssa,
  // uusin (newest) traileri on se (the one), joka (that) hallitsee (governs) tiedostoa
  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;  // ei klassista (classic) traileria: puhdas xref-virta-tiedosto, ei (not) hybridi

  // Hybridi (hybrid) traileri kantaa /XRefStm-avainta 'trailer'- ja 'startxref'-avainsanojen välissä
  KeyPos := PosEx('/XRefStm', Tail, TrailerPos);
  StartXrefPos := PosEx('startxref', Tail, TrailerPos);
  Result := (KeyPos > 0) and
    ((StartXrefPos = 0) or (KeyPos < StartXrefPos));
end;

Kolme tulosta (outcomes) ovat linjassa kolmen asettelun (layouts) kanssa. Pelkästään klassisessa (classic-only) tiedostossa (file) on (has a) traileri (trailer), mutta ei /XRefStm-avainta: False. Tiedostossa (file), joka (that) sitoutuu (commits) täysin ristiviittausvirtoihin (cross-reference streams), ei ole trailer-avainsanaa lainkaan, sen trailer-avaimet elävät virtasanakirjassa (stream dictionary): myös False, oikeutetusti (correctly), koska tällainen (such a) tiedosto on pakattu (compressed), ei (not) hybridi (hybrid). Vain (Only) kaksoisindeksoitu asettelu (double-indexed layout) palauttaa True

Tuotantokäyttöön (production use) kaksi (two) karkaisua (hardenings) ovat (are) ylimääräisten (extra) rivien (lines) arvoisia (worth). Jäsennä (Parse) kokonaisluku (integer) /XRefStm-avaimen jälkeen (after), hakeudu (seek to) kyseiseen siirtymään (offset), ja (and) vahvista (confirm), että virtaobjekti (stream object), jolla on (with) /Type /XRef, todella (actually) istuu siellä; katkaistu tiedosto (truncated file) voi (can) kantaa (carry) avainta (key), vaikka virta (stream) on (is) poissa (gone), mikä (which) kuuluu eri sankoon (bucket) kuin terve (healthy) hybridi (hybrid). Ja käsittele ikkunan kokoa (window size) parametrina (parameter): 2 KB kattaa tavallisen Office-tulosteen (Office output), mutta (but) epätavallisen suuri (unusually large) trailer-sanakirja (trailer dictionary) voi työntää avainsanan alueen (range) ulkopuolelle (out of), ja (and) ikkunan leventäminen (widening) voittaa sen, että tiedosto julistetaan (declaring) vahingossa (by accident) klassiseksi

Hybriditiedostojen reitittäminen Delphi-putken läpi

Tunnistaminen (Detection) ostaa sinulle (buys you a) reitityspäätöksen (routing decision). Tiedostoille, joita vain luetaan (read), renderöidään (rendered) tai (or) validoidaan (validated), käytä lataajaa (loader), joka selvittää (resolves) molemmat näkymät (both views), ja (then) todenna (verify) toiminta (behavior) tavujen (bytes) sijaan (rather than). PDFium Component jäsentää (parses) /XRefStm-ketjun latauksen aikana (during load), joten (so) koodisi (code) näkemä objektitaulukko (object table) on (is) se (the) yhdistetty (merged one), ja (and) objekti- ja ristiviittausvirtojen validointia (validating) käsittelevässä artikkelissamme kuvatut (described) tarkistukset pätevät (apply) sellaisenaan (unchanged). Jos epäsynkronoitu hybridi (desynchronized hybrid) on vaurioitunut niin pahasti, että se kieltäytyy (refuse) latautumasta (loading), moottori (engine) raportoi sen (reports it) virhejoukkonsa (error set) kautta: FPDF_ERR_SUCCESS, FPDF_ERR_UNKNOWN, FPDF_ERR_FILE, FPDF_ERR_FORMAT, FPDF_ERR_PASSWORD, FPDF_ERR_SECURITY ja FPDF_ERR_PAGE, joista (with) FPDF_ERR_FORMAT on se (the one), jonka rakenteellinen vaurio (structural damage) tuottaa. Älä kuitenkaan (though) nojaa (lean on) tuohon signaaliin (signal): PDFium on suunnittelultaan (by design) salliva (lenient) ja (and) rakentaa useimmat epäjohdonmukaiset (inconsistent) tiedostot hiljaa (silently) uudelleen (rebuilds), joten (so a) onnistunut (successful) lataus (load) todistaa (proves), että tiedosto oli (was) palautettavissa (recoverable), ei (not that) sitä, että sen (its) kaksi näkymää (views) ovat samaa mieltä (agree). Mielekäs (meaningful) johdonmukaisuustarkistus (consistency check) on verrata (comparing) sitä (what a), mitä täydellinen (full) objektien läpikäynti (object walk) löytää, siihen, mitä (against what the) trailerin (trailer's) /Size julistaa (declares)

Putkesi (pipeline) muokkaamille (modifies) tiedostoille (files) turvallisin (safest) politiikka (policy) on lopettaa (stop) niiden oleminen (being) hybridejä (hybrid) ollenkaan (at all). Lataus (load), jota seuraa (followed by a) täysi tallennus (full save) HotPDF:n kautta (through), kirjoittaa (rewrites) asiakirjan uudelleen (with a) yhdellä, sisäisesti johdonmukaisella (self-consistent) ristiviittauksella (cross-reference) yhdessä (one) muodossa: ei (no) /XRefStm-avainta, ei (no) toista näkymää (second view), joka voisi joutua epäsynkroniin (fall out of sync), jokainen objekti on tasan yhden (exactly one) hakemistomerkinnän (index entry) omistama (owned by). Tuo normalisointi (normalization) on se (is what), mitä (you) haluat ennen arkiston sisäänottoa (archival ingest), ennen tiukkaa (strict) alavirran (downstream) RIP:iä (RIP) tai allekirjoituspalvelua (signing service), ja (and) minkä tahansa (any) hybridisyötteeseen sovelletun muokkauksen (edit applied) jälkeen (after). Se toimii, koska lataaja (loader) yhdisti (merged) näkymät (views) oikein sisään tullessa (on the way in); mekanismin (mechanism the), jonka HotPDF:n hybridiviite-artikkeli käy läpi (walks through) yksityiskohtaisesti (in detail)

Ainoa (The one) tiedostoluokka (class of files), joka (to) on syytä jättää (leave) rauhaan (alone), on digitaalisesti (digitally) allekirjoitetut asiakirjat (signed documents). Täysi uudelleenkirjoitus (full rewrite) siirtää jokaisen (every) tavun (byte), mikä (which) mitätöi (invalidates) minkä tahansa (any) alkuperäisten (original) alueiden (ranges) yli lasketun allekirjoituksen (signature computed over). Muutos (change) allekirjoitettuun (signed) hybridiin (hybrid) täytyy (must) mennä sisään (go in) oikeana (proper) inkrementaalisena (incremental) päivityksenä (update), joka ylläpitää (maintains) molemmat näkymät (both views); tiedoston, jota tarvitsee vain lukea (only needs reading), tulisi (should) kulkea läpi (pass through) koskemattomana (untouched). Normalisointi (Normalization) on tiedostoille (files), jotka omistat; allekirjoitettuihin tiedostoihin lisäät (append) aina vain sisältöä (only ever)

Hybridiviite-PDF:t (Hybrid-reference PDFs) eivät ole virheellisesti muodostettuja (malformed); ne (they) ovat formaatin oma (format's own) yhteensopivuussilta (compatibility bridge), ja (and) Office-sovellukset jatkavat (will keep) niiden tuottamista (producing them) niin kauan kuin PDF 1.4 -lukijat (readers) selviytyvät asennuskannassa (install base). Putki (pipeline), joka pystyy havaitsemaan (can spot the) /XRefStm-avaimen, validoimaan (validate) yhdistetyn asiakirjan (merged document) PDFium Componentilla ja luomaan puhtaan (clean), yksittäisindeksillä varustetun tulosteen (single-index output) HotPDF Componentin avulla, kohtelee (treats) niitä sinä mitä ne ovat: tavallisina (ordinary) syötteinä, joissa (with one) on yksi ylimääräinen (extra) tienviitta (signpost) trailerissa