Technisch artikel

Gecomprimeerde PDF's valideren: object- en xref-stromen

U schrijft een kleine validator. Deze opent een PDF, zoekt naar het einde, vindt startxref, leest de offset en verwacht te landen op het trefwoord xref met daaronder een kruisverwijzingstabel (cross-reference table) van vaste breedte. Uit die tabel verzamelt hij object-offsets en scant vervolgens achterwaarts naar het trefwoord trailer om de /Root en /Size te achterhalen. Dit werkt perfect op elk bestand dat u hebt gegenereerd om het te testen. Dan komt er een bestand binnen dat is geproduceerd door een huidige versie van Word, of door een bibliotheek die zich richt op PDF 1.5, en de validator verklaart het beschadigd. Er is geen trefwoord xref waar de offset naar verwijst, nergens is een trailer-woordenboek te bekennen, en de objecttabel die de validator heeft opgebouwd is nagenoeg leeg. Het bestand is echter geldig. De validator leest het door een vijftien jaar oude bril

Dit is de meest voorkomende reden waarom een byte-level PDF-controle die is geschreven tegen de klassieke lay-out, faalt op moderne documenten. De structuur waarvan deze afhankelijk is — de kruisverwijzingstabel in platte tekst en het trefwoord trailer — is vanaf PDF 1.5 optioneel gemaakt en ontbreekt vaak. Twee functies hebben dit vervangen: de kruisverwijzingsstroom (cross-reference stream) en de gecomprimeerde objectstroom (object stream). Beide worden beschreven in ISO 32000-1, en een validator die hier niets van afweet, ziet een gezond bestand aan voor een hoop ontbrekende objecten

Wat PDF 1.5 veranderde aan de staart van het bestand

ISO 32000-1 §7.5.8 definieert de kruisverwijzingsstroom en §7.5.7 definieert de objectstroom van het type /ObjStm. Samen stellen ze een schrijver in staat om de twee structuren waarop een klassieke parser zich richt, weg te laten. Een PDF 1.5-bestand kan eindigen zonder enige xref-tabel. In plaats daarvan is het object waarnaar startxref verwijst een gewoon stroomobject (stream object) waarvan het woordenboek /Type /XRef bevat, en die stroom bevat de kruisverwijzingsgegevens in een compacte binaire vorm. Er is ook geen trefwoord trailer meer, want de trailer is nu het eigen woordenboek van de stroom. De sleutels waar een klassieke parser naar zocht, /Root, /Size en /ID, bevinden zich in dat woordenboek

De tweede wijziging verplaatst de objecten zelf. In plaats van elk indirect object op zijn eigen byte-offset te schrijven, kan een schrijver veel kleine objecten — de paginadictionary's, de annotatiedictionary's, de structuurboom — in een enkele objectstroom verpakken en de hele container comprimeren met Flate. De afzonderlijke objecten hebben geen byte-offset meer in het bestand. Ze hebben een positie binnen een gecomprimeerde blob. Een validator die de ruwe bytes scant op 1 0 obj vindt ze nooit, omdat die tekst pas bestaat na het uitpakken (decompressie). Voor een klassieke parser is de helft van het document simpelweg verdwenen

De trailersleutels zijn in platte tekst, zelfs in een gecomprimeerd bestand

Het geruststellende is dat het lezen van de trailer van een kruisverwijzingsstroom geen enkele decompressie vereist. Een stroomobject wordt geschreven als een woordenboek gevolgd door het trefwoord stream en vervolgens de gecomprimeerde bytes. Het woordenboek is platte tekst. Dus wanneer startxref naar een kruisverwijzingsstroom verwijst, zien de bytes direct na het objectnummer eruit als een gewoon woordenboek, en bevinden /Root, /Size en /ID zich daar in klare tekst, voordat het trefwoord stream en de Flate-gegevens beginnen

Dat betekent dat een validator de drie feiten die hij het meest nodig heeft — waar de catalogus (catalog) is, hoeveel objecten het bestand claimt en de bestandsidentificatie — kan leren door alleen het stroomwoordenboek te ontleden. Hij hoeft de kruisverwijzingsgegevens niet te decompresseren en de binaire vermeldingen daarin niet te interpreteren. Het werk dat een naïeve parser de das omdoet, is niet het lezen van de trailer; het is het vinden van de objecten. Dat zijn twee van elkaar te scheiden problemen, en het oplossen van het eerste is goedkoop

Objectstromen: een header, dan een Flate-blob

Een objectstroom is een container. Het woordenboek bevat /Type /ObjStm, een vermelding /N die het aantal verpakte objecten daarin aangeeft, en een vermelding /First die de byte-offset geeft, binnen de uitgepakte gegevens, waar het lichaam van het eerste object begint. De gecomprimeerde payload begint, eenmaal uitgepakt, met een kleine header van /N paren gehele getallen. Elk paar is een objectnummer en de offset van het lichaam van dat object ten opzichte van /First. Na de header volgen de objectlichamen zelf, aaneengeschakeld

Het uitvouwen ervan is mechanisch werk zodra de bytes zijn uitgepakt. U leest het woordenboek om /N en /First te verkrijgen, pakt de stroom uit met een Flate-decoder, doorloopt de leidende /N-paren om te leren welk objectnummer op welke offset leeft, en tilt vervolgens elk lichaam eruit alsof het een gewoon indirect object was. De enige echte afhankelijkheid is de Flate-decoder, en die hebt u al: Delphi levert System.ZLib en Free Pascal levert de unit zstream, die beide zlib wrappen en een ruwe Flate-stroom uitpakken zonder code van derden. Een routine die elk geëxtraheerd object toevoegt aan de objecttabel van de validator, zorgt ervoor dat de rest van de validator — het deel dat /Root doorloopt en de paginaboom controleert — zich precies zo gedraagt als bij een klassiek bestand

Wat u niet hoeft te implementeren

Het is gemakkelijk om het werk te overschatten. Het lezen van de trailersleutels uit een gecomprimeerd bestand vereist geen decodering van de binaire vermeldingen van de kruisverwijzingsstroom. De §7.5.8 kruisverwijzingsstroom gebruikt drie typen vermeldingen, en de type 2-vermelding — die zegt dit object leeft binnen objectstroom N op index i — is wat u zou decoderen om een volledige offset-kaart te bouwen. U hebt die kaart nodig om willekeurige objecten op nummer op te lossen. U hebt hem niet nodig om /Root, /Size en /ID te lezen, die zich in het woordenboek in platte tekst bevinden, en u hebt hem niet nodig om objectstromen uit te vouwen, omdat elke /ObjStm zijn eigen inhoud aankondigt via /N en /First

U hoeft ook niet de PNG- en TIFF-predictorfuncties af te handelen die een kruisverwijzingsstroom via haar /DecodeParms kan toepassen, enkel om de trailersleutels te verkrijgen. Predictors filteren de binaire kruisverwijzingsrijen om ze beter te comprimeren; ze hebben niets te maken met het woordenboek dat voorafgaat aan de stroom. De minimale upgrade die een klassieke validator bewust maakt van moderne PDF's is daarom klein: wanneer startxref op een stroom landt in plaats van op het trefwoord xref, ontleed dan het stroomwoordenboek voor de trailersleutels, en vouw alle /ObjStm-objecten uit die u tegenkomt, zodat hun inhoud in de objecttabel terechtkomt. Het decoderen van type 2-vermeldingen en predictors is een aparte, grotere taak die u kunt uitstellen totdat u daadwerkelijk willekeurige objectresolutie nodig hebt

Waarom een nalevingscontrole eerst stromen moet uitvouwen

Dit houdt op academisch te zijn op het moment dat u een profielcontrole uitvoert. Een PDF/A- of PDF/X-validator inspecteert specifieke objecten: de documentcatalogus voor een /OutputIntents-array, de /Metadata-stroom voor een XMP-pakket met de juiste identificator, elke fontdescriptor voor een ingebed lettertypebestand, de trailer voor een /ID. In een gecomprimeerd bestand bevinden de meeste van die objecten zich in objectstromen. Een validator die de objectstromen niet heeft uitgevouwen, kan de sleutels van de catalogus niet zien, de metadata niet vinden en de lettertypen niet enumereren. Hij zal een perfect conform document rapporteren als ontbrekend aan output intent, ontbrekend aan XMP en ontbrekend aan de helft van zijn structuur, omdat het bewijs dat hij nodig heeft nog steeds in een Flate-blob zit die hij nooit heeft uitgepakt

De volgorde is belangrijk. Het uitvouwen moet gebeuren voordat de controles worden uitgevoerd, niet ernaast, omdat elke controle ervan uitgaat dat hij een object op nummer kan bereiken. Als u een profielcontrole rechtstreeks op een ruwe byte-scan aansluit, erft deze de blindheid van de klassieke parser en produceert deze valse schendingen op juist de moderne bestanden die het meest waarschijnlijk goed gevormd zijn, aangezien ze afkomstig zijn uit toolchains die nieuw genoeg zijn om kruisverwijzingsstromen überhaupt te schrijven

PDFium het ontleedwerk voor u laten doen

Het PDFium Component ontleedt kruisverwijzingsstromen en objectstromen als onderdeel van het laden van een document, wat de praktische manier is om het handmatig decoderen en uitvouwen te vermijden. Wanneer u een bestand laadt met het component TPdf, zijn de objecten die in /ObjStm-containers zijn verpakt al opgelost, en zien de validatie-invoerpunten het volledig uitgevouwen document. ValidatePdfA retourneert een TPdfAValidationResult-record waarvan het veld Conformance een TPdfAConformance-waarde is zoals pac1b of pacNone, waarvan het veld Issues een set is van de specifieke gevonden problemen, en waarvan de methode IsCompliant alleen True is wanneer een nalevingsniveau is gedetecteerd en de set met problemen leeg is. Omdat de objecten tijdens het laden zijn uitgevouwen, wordt een /OutputIntents-array of een ingebed lettertype dat zich in een objectstroom bevond gevonden, en niet als ontbrekend gerapporteerd

uses
  PDFium, FPdfPdfa;

function CheckPdfA(const FileName: string): TPdfAValidationResult;
var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := FileName;
    Pdf.Active := True;            // ontleedt xref/objectstromen bij het laden
    Result := Pdf.ValidatePdfA;    // ziet de uitgevouwen objecttabel
  finally
    Pdf.Free;
  end;
end;

Hetzelfde geldt voor ValidatePdfX, dat een TPdfXValidationResult met dezelfde vorm retourneert. Het voordeel van het omleiden via PDFium is dat de hierboven beschreven structurele decompressie eenmalig, correct plaatsvindt binnen de lader, zodat uw validatiecode nooit het verschil ziet tussen een klassiek bestand en een volledig gecomprimeerd bestand. Beide komen bij de validator aan als een opgeloste set objecten

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 := 'geen';
  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 een set: tel de leden ervan
        Inc(IssueCount);
      Writeln('Niet conform; aantal problemen = ', IssueCount);
    end;
  finally
    Pdf.Free;
  end;
end;

Als de bytes zich al in het geheugen bevinden in plaats van op de schijf, werkt dezelfde laad-en-valideervolgorde via de overload LoadDocument(const Data: TBytes), die de ruwe bestandsinhoud neemt en de kruisverwijzings- en objectstromen op dezelfde manier ontleedt als het bestandspad doet. De les voor een handgeschreven validator is de structurele regel, niet de API: lees de trailersleutels uit het stroomwoordenboek in platte tekst, vouw elke /ObjStm uit met een Flate-decoder voordat u het document doorloopt, en behandel het decoderen van de binaire kruisverwijzingsvermeldingen als de grotere, optionele taak die het is

Zodra de structuur is uitgevouwen, kan een validator de rest van een workflow daarover aansturen. Voor een preflight-harnas op de opdrachtregel dat naleving rapporteert voor een map met invoerbestanden, zie onze handleiding over het bouwen van een batch preflight-rapport CLI. Wanneer validatie een poort is voordat een groot document uit elkaar wordt gehaald, sluiten de technieken in onze handleiding voor het splitsen van PDF-documenten in meerdere bestanden natuurlijk aan bij het hier getoonde laad-en-controlepatroon. Beide bouwen voort op het laad- en validatie-oppervlak van het PDFium Component voor Delphi en C++Builder