U schrijft een kleine validator. Deze opent een PDF, zoekt naar het einde, vindt startxref, leest de offset, en verwacht uit te komen op het trefwoord xref met daaronder een kruisverwijzingstabel (cross-reference table) van vaste breedte. Uit die tabel verzamelt hij objectoffsets, 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 te testen. Maar dan komt er een bestand binnen dat is gemaakt met een recente versie van Word, of door een bibliotheek die zich richt op PDF 1.5, en de validator verklaart het bestand corrupt. Er is geen xref-trefwoord waar de offset naar verwijst, nergens een trailer-dictionary te vinden, en de objecttabel die de validator heeft opgebouwd is vrijwel leeg. Het bestand is echter geldig. De validator leest het door een vijftien jaar oude bril.
Dit is de meest voorkomende reden waarom een PDF-controle op byte-niveau die is geschreven voor de klassieke layout, faalt op moderne documenten. De structuur waarvan deze afhankelijk is, de kruisverwijzingstabel in platte tekst en het trefwoord trailer, werd optioneel gemaakt in PDF 1.5 en is vaak afwezig. Twee functies hebben dit vervangen: de kruisverwijzingsstroom (cross-reference stream) en de gecomprimeerde objectstroom. Beide worden beschreven in ISO 32000-1, en een validator die er niet vanaf weet, ziet een gezong bestand als een verzameling ontbrekende objecten.
Wat PDF 1.5 veranderde aan het einde 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 writer in staat de twee structuren waarop een klassieke parser leunt, 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 de dictionary /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 de dictionary van de stroom zelf. De sleutels waarnaar een klassieke parser zocht, /Root, /Size en /ID, bevinden zich binnen die dictionary.
De tweede wijziging verplaatst de objecten zelf. In plaats van elk indirect object op zijn eigen byte-offset te schrijven, een writer kan veel kleine objecten (de paginadictionaries, de annotatiedictionaries, de structuurboom) verpakken in een enkele objectstroom en de hele container comprimeren met Flate. De afzonderlijke objecten hebben niet langer een byte-offset 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 decompressie (inflation). Voor een klassieke parser is de helft van het document simpelweg verdwenen.
De trailer-sleutels zijn platte tekst, zelfs in een gecomprimeerd bestand
Het geruststellende deel is dat het lezen van de trailer van een kruisverwijzingsstroom geen decompressie vereist. Een stroomobject wordt geschreven als een dictionary gevolgd door het trefwoord stream en vervolgens de gecomprimeerde bytes. De dictionary is platte tekst. Dus wanneer startxref naar een kruisverwijzingsstroom verwijst, de bytes direct na het objectnummer zien eruit als een gewone dictionary, en /Root, /Size en /ID staan daar in het zicht, nog voorafgaand aan het trefwoord stream en de Flate-gegevens beginnen.
Dat betekent dat een validator de drie gegevens die hij het meest nodig heeft (waar de catalogus is, hoeveel objecten het bestand claimt en de bestandsidentificatie) kan achterhalen door alleen de stroomdictionary te parseren. He hoeft de kruisverwijzingsgegevens niet te decompileren en hij hoeft de binaire vermeldingen erin niet te interpreteren. Het werk waar een eenvoudige parser op vastloopt is niet het lezen van de trailer; het is het vinden van de objecten. Dat zijn twee gescheiden problemen, en het oplossen van de eerste is eenvoudig.
Objectstromen: een header, en dan een Flate-blob
Een objectstroom is een container. De dictionary ervan bevat /Type /ObjStm, een /N-vermelding die het aantal verpakte objecten aangeeft, en een /First-vermelding die de byte-offset geeft, binnen de gedecomprimeerde gegevens, waar de body van het eerste object begint. De gecomprimeerde payload begint, eenmaal gedecomprimeerd, met een kleine header van /N integer-paren. Elk paar is een objectnummer en de offset van de body van dat object ten opzichte van /First. Na de header volgen de objectbodys zelf, achter elkaar geplaatst.
Het uitpakken ervan is mechanisch zodra de bytes zijn gedecomprimeerd. U leest de dictionary om /N en /First te verkrijgen, decomprimeert de stroom met een Flate-decoder, doorloopt de leidende /N-paren om te leren welk objectnummer zich op welke offset bevindt, en haalt vervolgens elke body eruit alsof het een gewoon indirect object is. De enige echte afhankelijkheid is de Flate-decoder, en u hebt er al een: Delphi levert System.ZLib, en Free Pascal levert de zstream-unit. Beide maken gebruik van zlib en decompileren een ruwe Flate-stroom 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 exact 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 trailer-sleutels 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 (degene die zegt: 'dit object bevindt zich in objectstroom N op index i') is wat u zou decoderen om een volledige offsetkaart te bouwen. U hebt die kaart nodig om willekeurige objecten op nummer op te lossen. U hebt deze niet nodig om /Root, /Size en /ID te lezen, die zich in de platte-tekstdictionary bevinden, en u hebt deze niet nodig om objectstromen uit te pakken, 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 kan toepassen via de /DecodeParms om louter de trailer-sleutels te verkrijgen. Predictors filteren de binaire kruisverwijzingsrijen om ze beter te comprimeren; ze hebben niets te maken met de dictionary die aan de stroom voorafgaat. 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, parseer dan de stroomdictionary voor de trailer-sleutels, en pak alle /ObjStm-objecten uit die u tegenkomt zodat hun inhoud in de objecttabel wordt opgenomen. De coderen van type 2-vermeldingen en predictors is een aparte, grotere taak die u kunt uitstellen tot u daadwerkelijk willekeurige objectresolutie nodig hebt.
Waarom een nalevingscontrole eerst stromen moet uitpakken
Dit stopt met 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 identificatie, elke fontdescriptor voor een ingebed fontbestand, 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 uitgepakt, kan de sleutels van de catalogus niet zien, kan de metadata niet vinden en kan de lettertypen niet inventariseren. Deze zal een perfect conform document rapporteren als zijnde zonder uitvoerintentie (output intent), zonder XMP en zonder de helft van de structuur, omdat het bewijs dat nodig is zich nog bevindt in een Flate-blob die nooit is gedecomprimeerd.
De volgorde is belangrijk. Het uitpakken moet gebeuren voordat de controles worden uitgevoerd, en niet tegelijkertijd, omdat elke controle ervan uitgaat dat deze een object op nummer kan bereiken. Als u een profielcontrole rechtstreeks koppelt aan een ruwe bytescan, erft deze de blindheid van de klassieke parser over en produceert valse waarschuwingen op juist die moderne bestanden die waarschijnlijk goed zijn opgebouwd, aangezien ze afkomstig zijn uit toolchains die nieuw genoeg zijn om kruisverwijzingsstromen te schrijven.
PDFium de parsering voor u laten doen
Het PDFium Component parseert kruisverwijzingsstromen en objectstromen als onderdeel van het laden van een document, wat de praktische manier is om te voorkomen dat u de decompressie- en uitpakstap zelf moet schrijven. Wanneer u een bestand laadt met het TPdf-component, de objects verpakt in /ObjStm containers zijn al opgelost, en de validatietoegangspunten zien het volledig uitgepakte document. ValidatePdfA retourneert een TPdfAValidationResult-record waarvan het Conformance-veld een TPdfAConformance-waarde is zoals pac1b of pacNone, waarvan het Issues-veld een set is van de specifieke gevonden problemen, en waarvan de IsCompliant-methode alleen true is wanneer er een conformiteitsniveau is gedetecteerd en de set met problemen leeg is. Omdat de objecten tijdens het laden zijn uitgepakt, een /OutputIntents-array of een ingebed font dat zich in een objectstroom bevond wel 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; // parses xref/object streams on load
Result := Pdf.ValidatePdfA; // sees the expanded object table
finally
Pdf.Free;
end;
end;
Hetzelfde geldt voor ValidatePdfX, dat een TPdfXValidationResult met dezelfde vorm retourneert. Het doel van het routeren via PDFium is dat de hierboven beschreven structurele decompressie eenmalig, correct, binnen de loader gebeurt, 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.
var
Pdf: TPdf;
R : TPdfXValidationResult;
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: ', Ord(R.Conformance))
else
Writeln('Not conformant; issue count = ', SizeOf(R.Issues));
finally
Pdf.Free;
end;
end;
Zodat de structuur is uitgepakt, kan een validator de rest van de workflow eroverheen aansturen. Voor een command-line preflight-omgeving via de commandline die de conformiteit rapporteert over een map met invoerbestanden, zie onze handleiding over het bouwen van een batch-preflight-rapportage-CLI. Wanneer validatie een drempel is voordat u een groot document opsplitst, de technieken in onze gids over het splitsen van PDF-documenten in meerdere bestanden sluiten natuurlijk aan bij het hier getoonde laad-en-controlepatroon. Beide bouwen voort op de laad- en validatie-interface van het PDFium Component voor Delphi en C++Builder.