Techninis straipsnis

Suglaudintų PDF tikrinimas: objektų ir XRef srautai

Jūs rašote nedidelį tikrintuvą (validator). Jis atidaro PDF, pereina į pabaigą, randa startxref, nuskaito poslinkį ir tikisi atsidurti ties raktažodžiu xref su po juo esančia fiksuoto pločio kryžminių nuorodų lentele. Iš tos lentelės jis surenka objektų poslinkius, o tada nuskaito atgal ieškodamas raktažodžio trailer, kad sužinotų /Root ir /Size. Tai veikia tobulai su kiekvienu failu, kurį sugeneravote testavimui. Tada gaunamas failas, sukurtas naudojant dabartinę „Word“ versiją arba biblioteką, kuri skirta PDF 1.5, ir tikrintuvas paskelbia jį sugadintu. Nėra jokio xref raktažodžio ten, kur rodo poslinkis, niekur nėra jokio trailer žodyno, o tikrintuvo sukurtas objektų sąrašas yra beveik tuščias. Failas yra galiojantis (valid). Tikrintuvas jį skaito per penkiolikos metų senumo prizmę

Tai yra dažniausia priežastis, dėl kurios baitų lygio PDF patikrinimas, parašytas klasikiniam išdėstymui, nepavyksta su šiuolaikiniais dokumentais. Struktūra, nuo kurios jis priklauso – paprasto teksto kryžminių nuorodų lentelė ir raktažodis trailer – PDF 1.5 versijoje tapo neprivaloma ir dažnai jos nebūna. Ją pakeitė dvi savybės: kryžminių nuorodų srautas (cross-reference stream) ir suglaudintas objektų srautas (compressed object stream). Abi jos aprašytos ISO 32000-1 standarte, ir tikrintuvas, kuris apie jas nežino, mato sveiką failą kaip trūkstamų objektų krūvą

Ką PDF 1.5 pakeitė failo pabaigoje

ISO 32000-1 standarto §7.5.8 skyrius apibrėžia kryžminių nuorodų srautą, o §7.5.7 skyrius apibrėžia /ObjStm tipo objektų srautą. Kartu jos leidžia rašytojui (writer) atsisakyti tų dviejų struktūrų, kuriomis remiasi klasikinis analizatorius (parser). PDF 1.5 failas gali baigtis be jokios xref lentelės. Vietoje jos, objektas, į kurį rodo startxref, yra paprastas srauto objektas, kurio žodyne yra /Type /XRef, o tas srautas saugo kryžminių nuorodų duomenis kompaktišku dvejetainiu formatu. Raktažodžio trailer taip pat nėra, nes pabaigos elementas dabar yra paties srauto žodynas. Raktai, kurių ieškojo klasikinis analizatorius – /Root, /Size ir /ID – yra tame žodyne

Antrasis pakeitimas perkelia pačius objektus. Užuot rašęs kiekvieną netiesioginį objektą ties jo paties baito poslinkiu, rašytojas gali supakuoti daugelį mažų objektų – puslapių žodynus, anotacijų žodynus, struktūros medį – į vieną objektų srautą ir suglaudinti visą konteinerį su Flate. Atskiri objektai faile nebeturi baitų poslinkio. Jie turi poziciją suglaudinto masyvo viduje. Tikrintuvas, skenuojantis neapdorotus baitus ir ieškantis 1 0 obj, niekada jų neranda, nes šis tekstas egzistuoja tik po išglaudinimo (inflation). Klasikiniam analizatoriui pusė dokumento paprasčiausiai išnyko

Pabaigos raktai (trailer keys) yra paprastas tekstas, net ir suglaudintame faile

Raminanti dalis yra ta, kad norint nuskaityti kryžminių nuorodų srauto pabaigos elementą, nereikia nieko išglaudinti. Srauto objektas įrašomas kaip žodynas, po kurio eina raktažodis stream, o tada suglaudinti baitai. Žodynas yra paprastas tekstas (plaintext). Todėl, kai startxref rodo į kryžminių nuorodų srautą, baitai iškart po objekto numerio atrodo kaip paprastas žodynas, ir /Root, /Size bei /ID guli ten atvirai, dar prieš prasidedant raktažodžiui stream ir Flate duomenims

Tai reiškia, kad tikrintuvas gali sužinoti tris jam labiausiai reikalingus faktus – kur yra katalogas, kiek objektų teigia turįs failas, ir failo identifikatorių – analizuodamas tik srauto žodyną. Jam nereikia išglaudinti kryžminių nuorodų duomenų ir interpretuoti viduje esančių dvejetainių įrašų. Darbas, kuris nugali naivų analizatorių, nėra pabaigos nuskaitymas; tai yra objektų radimas. Tai yra dvi atskiros problemos, ir pirmosios išsprendimas yra pigus

Objektų srautai: antraštė, po to Flate masyvas

Objektų srautas yra konteineris. Jo žodyne yra /Type /ObjStm, /N įrašas, nurodantis viduje supakuotų objektų skaičių, ir /First įrašas, nurodantis baito poslinkį išglaudintuose duomenyse, kur prasideda pirmojo objekto kūnas. Suglaudinta naudingoji apkrova, kai ji išglaudinama, prasideda maža antrašte iš /N sveikųjų skaičių porų. Kiekviena pora – tai objekto numeris ir to objekto kūno poslinkis /First atžvilgiu. Po antraštės eina patys objektų kūnai, sujungti vienas po kito

Jų išplėtimas yra mechaninis, kai baitai išglaudinami. Jūs nuskaitote žodyną, kad gautumėte /N ir /First, išglaudinate srautą naudodami Flate dekoderį, praeinate pro pirmaujančias /N poras, kad sužinotumėte, koks objekto numeris gyvena kokiame poslinkyje, o tada ištraukiate kiekvieną kūną taip, tarsi tai būtų paprastas netiesioginis objektas. Vienintelė tikra priklausomybė yra Flate dekoderis, o jūs jį jau turite: Delphi pristato System.ZLib, o Free Pascal pristato zstream modulį, kurie abu apgaubia (wrap) zlib ir išglaudina neapdorotą Flate srautą be jokio trečiųjų šalių kodo. Procedūra, kuri prideda kiekvieną ištrauktą objektą į tikrintuvo objektų lentelę, priverčia likusią tikrintuvo dalį, tą dalį, kuri pereina per /Root ir tikrina puslapių medį, elgtis lygiai taip pat, kaip ji elgtųsi su klasikiniu failu

Ko jums nereikia realizuoti

Lengva pervertinti darbą. Norint nuskaityti pabaigos raktus iš suglaudinto failo, nereikia dekoduoti kryžminių nuorodų srauto dvejetainių įrašų. §7.5.8 kryžminių nuorodų srautas naudoja tris įrašų tipus, o 2 tipo įrašas – tas, kuris sako šis objektas gyvena objektų sraute N ties indeksu i, yra tai, ką turėtumėte dekoduoti, kad sukurtumėte pilną poslinkių žemėlapį. Jums to žemėlapio reikia, kad galėtumėte atpažinti savavališkus objektus pagal numerį. Jums jo nereikia, norint nuskaityti /Root, /Size ir /ID, kurie yra atvirojo teksto žodyne, ir jo nereikia, norint išplėsti objektų srautus, nes kiekvienas /ObjStm paskelbia savo turinį per /N ir /First

Taip pat neprivalote apdoroti PNG ir TIFF prediktoriaus (predictor) funkcijų, kurias kryžminių nuorodų srautas gali pritaikyti per /DecodeParms, vien tam, kad gautumėte pabaigos raktus. Prediktoriai filtruoja dvejetaines kryžminių nuorodų eilutes, kad jos geriau susispaustų; jie neturi nieko bendro su žodynu, kuris yra prieš srautą. Todėl minimalus atnaujinimas, leidžiantis klasikiniam tikrintuvui suprasti šiuolaikinius PDF, yra nedidelis: kai startxref atsiduria ties srautu, o ne ties raktažodžiu xref, išanalizuokite srauto žodyną ieškodami pabaigos raktų ir išplėskite visus aptiktus /ObjStm objektus, kad jų turinys patektų į objektų lentelę. 2 tipo įrašų ir prediktorių dekodavimas yra atskira, didesnė užduotis, kurią galite atidėti, kol jums tikrai prireiks atsitiktinės objektų rezoliucijos

Kodėl atitikties patikrinimas pirmiausia turi išplėsti srautus

Tai nustoja būti vien tik teorija tą akimirką, kai paleidžiate profilio patikrinimą. PDF/A arba PDF/X tikrintuvas tikrina konkrečius objektus: dokumento katalogą dėl /OutputIntents masyvo, /Metadata srautą dėl XMP paketo su teisingu identifikatoriumi, kiekvieną šrifto deskriptorių dėl įterpto šrifto failo, pabaigos elementą dėl /ID. Suglaudintame faile dauguma tų objektų yra objektų srautuose. Tikrintuvas, kuris neišplėtė objektų srautų, negali matyti katalogo raktų, negali rasti metaduomenų ir negali išvardinti šriftų. Jis praneš, kad tobulai atitinkančiam dokumentui trūksta išvesties ketinimo (output intent), trūksta XMP ir trūksta pusės struktūros, nes jam reikalingi įrodymai vis dar slypi Flate masyve, kurio jis niekada neišglaudino

Tvarka yra svarbi. Išplėtimas turi įvykti prieš atliekant patikrinimus, o ne kartu su jais, nes kiekvienas patikrinimas daro prielaidą, kad jis gali pasiekti objektą pagal numerį. Jei susiesite profilio patikrinimą tiesiogiai su neapdorotu baitų skenavimu, jis paveldės klasikinio analizatoriaus aklumą ir pateiks klaidingus pažeidimus būtent su tais šiuolaikiniais failais, kurie greičiausiai yra gerai suformuoti, nes jie atsirado iš įrankių grandinių, kurios yra pakankamai naujos, kad apskritai įrašytų kryžminių nuorodų srautus

Leidžiame PDFium atlikti analizavimą už jus

PDFium komponentas išanalizuoja kryžminių nuorodų srautus ir objektų srautus įkeldamas dokumentą, o tai yra praktiškas būdas išvengti rankinio išglaudinimo ir išplėtimo žingsnio kūrimo. Kai įkeliate failą su TPdf komponentu, objektai, supakuoti į /ObjStm konteinerius, jau yra atpažinti, o tikrinimo įvesties taškai mato visiškai išplėstą dokumentą. ValidatePdfA grąžina TPdfAValidationResult įrašą, kurio laukas Conformance yra TPdfAConformance reikšmė, pavyzdžiui, pac1b arba pacNone, kurio Issues laukas yra rastų konkrečių problemų rinkinys, o jo metodas IsCompliant yra true (teisingas) tik tada, kai aptinkamas atitikties lygis, o problemų rinkinys yra tuščias. Kadangi įkėlimo metu objektai buvo išplėsti, randamas /OutputIntents masyvas arba įterptas šriftas, buvęs objektų sraute, o ne pranešama apie jų trūkumą

uses
  PDFium, FPdfPdfa;

function CheckPdfA(const FileName: string): TPdfAValidationResult;
var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := FileName;
    Pdf.Active := True;            // parses xref/object streams on load
    Result := Pdf.ValidatePdfA;    // sees the expanded object table
  finally
    Pdf.Free;
  end;
end;

Tas pats taikoma ir ValidatePdfX, kuris grąžina tokios pat formos TPdfXValidationResult. Maršrutizavimo per PDFium esmė yra ta, kad anksčiau aprašytas struktūrinis išglaudinimas įkėliklyje atliekamas vieną kartą ir teisingai, todėl jūsų tikrinimo kodas niekada nemato skirtumo tarp klasikinio failo ir visiškai suglaudinto failo. Abu jie patenka į tikrintuvą kaip išspręstas objektų rinkinys

function PdfXConformanceName(C: TPdfXConformance): string;
begin
  case C of
    pxc1a: Result := 'PDF/X-1a';
    pxc3 : Result := 'PDF/X-3';
    pxc4 : Result := 'PDF/X-4';
  else
    Result := 'none';
  end;
end;

var
  Pdf: TPdf;
  R  : TPdfXValidationResult;
  Issue: TPdfXValidationIssue;
  IssueCount: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'Press_Ready.pdf';
    Pdf.Active := True;
    R := Pdf.ValidatePdfX;
    if R.IsCompliant then
      Writeln('PDF/X conformance: ', PdfXConformanceName(R.Conformance))
    else
    begin
      IssueCount := 0;
      for Issue in R.Issues do   // Issues is a set: count its members
        Inc(IssueCount);
      Writeln('Not conformant; issue count = ', IssueCount);
    end;
  finally
    Pdf.Free;
  end;
end;

Jei baitai jau yra atmintyje, o ne diske, ta pati įkėlimo ir po to tikrinimo seka veikia per LoadDocument(const Data: TBytes) perkrovimą (overload), kuris paima neapdorotą failo turinį ir išanalizuoja jo kryžminių nuorodų bei objektų srautus taip pat, kaip tai daro failo kelias. Esminis dalykas rankiniu būdu parašytam tikrintuvas yra struktūrinė taisyklė, o ne API: nuskaitykite pabaigos raktus iš srauto žodyno atviru tekstu, išplėskite kiekvieną /ObjStm naudodami Flate dekoderį prieš tai, kai pereisite dokumentą, ir vertinkite dvejetainių kryžminių nuorodų įrašų dekodavimą kaip didesnį ir pasirenkamą darbą, koks jis ir yra

Kai struktūra išplėsta, tikrintuvas gali pagal ją vykdyti likusią darbo eigos dalį. Norėdami pamatyti komandų eilutės išankstinio tikrinimo („preflight“) rėmus, kurie pateikia ataskaitas apie įvesties aplanko atitiktį, peržiūrėkite mūsų vadovą apie paketinės „preflight“ ataskaitos CLI kūrimą. Kai tikrinimas yra vartai prieš padalijant didelį dokumentą, mūsų PDF dokumentų padalijimo į kelis failus vadovo metodai natūraliai dera su čia parodytu įkėlimo ir patikrinimo modeliu. Abu jie remiasi PDFium komponento įkėlimo ir patikrinimo paviršiumi, skirtu Delphi ir C++Builder