Technisch artikel

PDF/A-implementatielimieten en font-coderingscontroles

Een factuurarchief kan schoon renderen, printen, elk font insluiten en een geldige output intent dragen, en een PDF/A-validator weigert het toch omdat één resource-woordenboek 4.096 entries bevat. Het plafond ligt op 4.095. PDF/A erft een kleine tabel met implementatielimieten uit de PDF Reference, en een bestand dat er met één unit overheen gaat, faalt op conformiteit ook al klaagt geen enkele viewer

Het andere falen dat in hetzelfde emmertje "maar het oogt prima" belandt, is een symbolisch TrueType-font dat een /Encoding-entry draagt. Vinkjes, streepjescodes en dingbats komen goed op het scherm, en de validator vlagt het fontwoordenboek toch. Beide problemen wonen in de objectgraaf in plaats van in iets dat een mens op de pagina kan zien, dus ze zijn het mechanisch controleren waard voordat een bestand in een archief gaat

De getallen die PDF/A erft, en waar de grens ligt

PDFium Component controleert vier containerlimieten bij elke PDF/A-validatie: een naamtoken mag hooguit 127 bytes lang zijn, een array mag hooguit 8.191 elementen bevatten, een woordenboek mag hooguit 4.095 entries bevatten, en arrays en woordenboeken mogen hooguit 28 niveaus diep nesten. Alle vier de grenzen zijn inclusief. Een naam van 127 bytes passeert en een naam van 128 bytes faalt; een array van precies 8.191 elementen passeert en één element meer doet hem falen

De naam-, array- en woordenboeklimieten komen uit de tijd waarin een conforme lezer alleen structuren tot die groottes hoefde te verwerken. Een huidige desktopviewer kan een veel grotere array zonder klagen openen, maar van een archiefbestand wordt verwacht dat het opent in lezers die nog niemand heeft getest, inclusief oude lezers die een recordsysteem misschien nog draait. Daarom houdt PDF/A de historische plafonds aan in plaats van ze te verslappen

Inclusief tegenover exclusief is waar handgemaakte controles meestal misgaan. Een test geschreven als Count >= 8191 in plaats van Count > 8191 weigert bestanden die conformeren, en een validator die conforme bestanden weigert wordt desondanks geloofd, dus de generator wordt "gefixt" voor een probleem dat hij nooit had. De functie ValidatePdfACompliance in de unit FPdfPdfa telt pas voorbij de limiet, en hij leest de bestandsbytes direct, dus de controle draait zonder de PDFium-DLL te laden

Diagram van de vier inclusieve containerlimieten die PDFium Component controleert in een bytescan vanuit Delphi: naamtokens van 127 bytes, 8191 array-elementen, 4095 woordenboekentries en 28 niveaus van array- en woordenboeknesting
Alle vier de limieten zijn inclusief — de grenswaarde zelf valideert en pas de volgende unit faalt
uses
  System.Classes, FPdfPdfa;

procedure CheckContainerLimits(const FileName: string; Log: TStrings);
var
  Src: TFileStream;
  Res: TPdfAValidationResult;
begin
  Src := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
  try
    Res := ValidatePdfACompliance(Src);   // bytescan, geen PDFium-DLL nodig
    if pvaiNameOverLimit in Res.Issues then
      Log.Add('A name token is longer than 127 bytes');
    if pvaiArrayOverLimit in Res.Issues then
      Log.Add('An array holds more than 8191 elements');
    if pvaiDictOverLimit in Res.Issues then
      Log.Add('A dictionary holds more than 4095 entries');
    if pvaiNestingOverLimit in Res.Issues then
      Log.Add('Arrays and dictionaries nest deeper than 28 levels');
    if Res.IsCompliant then
      Log.Add('No PDF/A findings');
  finally
    Src.Free;
  end;
end;

De vier vlaggen zijn leden van de set Issues op TPdfAValidationResult, naast elke andere PDF/A-uitkomst, en IsCompliant geeft alleen True terug wanneer er een PDF/A-identificatie is gevonden en de set leeg is. Een bestand dat helemaal geen PDF/A-identificatie draagt, wordt gecontroleerd tegen de strenge PDF/A-1-regelset, dus de containerlimieten gelden ook voor hem

Welke generatoren lopen tegen deze limieten op?

Bijna altijd degenen die structuur in een lus bouwen. Een formulier met enkele duizenden velden produceert een AcroForm-/Fields-array, of een pagina-/Annots-array, die voorbij 8.191 entries loopt. Een rapportengine die per gegenereerd object één font- of afbeeldingsresource registreert en nooit een bestaande entry hergebruikt, laat een /Resources-subwoordenboek voorbij 4.095 sleutels groeien. Een getagd document waarvan de structuurboom door recursie over een genest datamodel ontstaat, kan zonder dat iemand kijkt voorbij 28 niveaus gaan

Lange namen hebben een andere oorsprong: data die in naamtokens wordt gepropt. Een separation-colorant genoemd naar een klantreferentie, een optional-content-groep genoemd naar een bestandspad, een veldlabel opgebouwd uit zes hiërarchieniveaus. Namen kosten niets om te genereren, en 127 bytes zijn snel bereikt zodra er een UTF-8-label met geaccenteerde tekens meedoet, want elk van die tekens kost twee of drie bytes. De 127 bytes worden geteld na de /-introductor, tot de volgende delimiter of witruimte

De remedie is elke keer structureel. Splits de array, splits het woordenboek, maak de recursie vlak, verkort de naam. PDF/A-markers in het bestand injecteren helpt bij geen van deze uitkomsten, want het probleem is de vorm van de objectgraaf en de XMP-metadata zegt er niets over

Hoe de scan elementen en entries telt

De telregel telt wanneer u een uitkomst in een hex-editor probeert te reproduceren. Een geneste array of woordenboek telt als één element van zijn ouder, zo is de limiet geformuleerd: een array van 10 elementen die elk 1.000-element-arrays zijn, is prima, want geen enkele array overtreft 8.191. Binnen een woordenboek worden alleen de sleutels geteld, dus een waarde die toevallig een naam is, blaast het entrytotaal niet op. Hex-tekenreeksen zoals <FEFF0041> worden als losse tokens overgeslagen, dus hun hoekhaakjes worden nooit aangezien voor woordenboeknesting

De scan dekt de objectstructuur van het hele bestand, inclusief objecten die in gecomprimeerde objectstreams zijn bewaard en die vóór het tellen worden uitgepakt. Streamlichamen zelf, zoals pagina-inhoud en afbeeldingsdata, worden eerst leeggemaakt, dus een reeks binary-bytes die toevallig een / of een [ bevat, kan geen valse naam- of array-uitkomst triggeren

Er is een aangrenzende numerieke limiet die dezelfde validatie onder een eigen vlag meldt. Wanneer het bestand PDF/A-2 of een later deel declareert, wordt pvaiIntegerImplementationLimit gezet voor elk integer-token buiten het 32-bits signed bereik, dat wil zeggen buiten -2.147.483.648 tot 2.147.483.647. Generatoren die byte-offsets of grote tellers als kale integers schrijven, zijn de gebruikelijke bron

Waarom moet een symbolisch TrueType-font /Encoding laten vallen?

ISO 19005-1 §6.3.7 staat voor een symbolisch TrueType-font alleen de ingebouwde cmap van het fontprogramma toe als karakterafbeelding. Een symbolisch font mapt codes op glyphs op eigen gezag; dat verklaart de Symbolic-vlag. Voeg een /Encoding-entry toe en het bestand geeft nu twee antwoorden op de vraag "welke glyph selecteert byte 0x41", zonder dat iets in het bestand zegt welke wint. Lezers resolven het conflict verschillend, en hetzelfde vinkje kan in de ene viewer als vinkje en in de andere als de letter A verschijnen

ValidatePdfACompliance vindt elk fontwoordenboek met /Subtype /TrueType, controleert of het een /Encoding-sleutel heeft, en leest daarna bit 3 (waarde 4) van de entry /Flags in de fontdescriptor. De descriptor wordt gevolgd of hij nu inline is geschreven of als indirect object wordt gerefereerd. Een niet-symbolisch TrueType-font dat /WinAnsiEncoding of /MacRomanEncoding houdt, wordt door deze controle niet gevlagd, want voor niet-symbolische fonts verwacht PDF/A precies die encoding

Diagram dat een symbolisch TrueType-font waarvan de /Encoding-entry twee concurrerende glyphafbeeldingen creëert afzet tegen een niet-symbolisch font dat terecht WinAnsiEncoding houdt, in een Delphi PDFium Component-controle
De controle leest de symbolische vlag uit de fontdescriptor en slaat toe op de contradictie, niet op de aanwezigheid van een encoding
// Res komt van ValidatePdfACompliance zoals in het eerste voorbeeld
if pvaiSymbolicTrueTypeEncoding in Res.Issues then
  Log.Add('A symbolic TrueType font carries /Encoding; PDF/A admits only ' +
    'the font''s built-in cmap (ISO 19005-1 6.3.7)');

// Een buurregel met een andere trigger: een NIET-symbolisch
// TrueType-font waarvan het Encoding-woordenboek /Differences draagt
if pvaiTrueTypeDifferencesForbidden in Res.Issues then
  Log.Add('A non-symbolic TrueType Encoding dictionary carries /Differences');

De fonts die deze regel triggeren zijn voorspelbaar: Symbol, Wingdings, streepjescode-fonts, iconfonts. Ze verschijnen in zakelijke documenten als vinkjes, sterren en streepjescodes, en een subsettingstap die elk TrueType-font gelijk behandelt, schrijft er ook een encoding voor weg. Het zijn ook de fonts waar niemand naar kijkt wanneer een validator "fontprobleem" zegt, want de fonts van de lopende tekst worden eerst gecontroleerd

De omgekeerde fout komt ook in het wild voor: een producent zet de Symbolic-vlag op een gewoon tekstfont dat in werkelijkheid WinAnsi-gecodeerd is. De PDF/A-controle volgt de clausule zoals geschreven en meldt dat font, en hier hoort de reparatie thuis in de descriptor: verbeter de vlag in de descriptor in plaats van de encoding te verwijderen. /Encoding uit een verkeerd gelabeld tekstfont halen zou de tekst laten afhangen van welke cmap de subset toevallig bevat, en dat is een slechter bestand dan waarmee u begon. Bekijk de glyphset van het font voordat u kiest welke helft van de contradictie eruit gaat

De uitkomsten lezen in een preflightrapport

De preflightrapport-unit wordt meegeleverd met de Enterprise-editie, terwijl ValidatePdfACompliance in elke editie zit. Het rapport in de unit FPdfPreflightReport sorteert de vier containerlimieten in de categorie Structure en de kwestie rond symbolische TrueType-encoding in de categorie Content. Die splitsing is bewust. Uitkomsten over structuur gaan meestal terug naar wie de generator onderhoudt, uitkomsten over inhoud meestal naar wie de fonts en sjablonen aanleverde, en een rapport dat ze mengt stuurt beide mensen achter problemen aan die niet van hen zijn

Elke issue draagt zijn enumnaam als Code en een aanbeveling die de remedie in concrete termen benoemt, bijvoorbeeld "Split dictionaries so none carries more than 4095 entries". Let op dat de categorie Structure ook twee andere uitkomsten bevat, een ontbrekende getagde structuur bij een Level A-claim en PDF 1.5-crossreference- of objectstreams in een PDF/A-1-bestand, dus filter op code wanneer u alleen de vier limieten wilt

uses
  System.SysUtils, System.Classes, PDFium, FPdfPreflightReport;

procedure ReportStructureFindings(const FileName: string; Log: TStrings);
var
  Pdf: TPdf;
  Report: TPdfPreflightReport;
  Issues: TPdfPreflightIssues;
  I: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := FileName;
    Pdf.Active := True;          // dit pad laadt de PDFium-DLL
    if not Pdf.Active then
      Exit;
    Report := BuildPdfPreflightReport(Pdf, [ppsPdfA]);
    Issues := Report.FilterByCategory('Structure').FlattenIssues;
    for I := 0 to High(Issues) do
      Log.Add(Issues[I].Code + ': ' + Issues[I].Recommendation);
    Report.SaveJsonToFile(ChangeFileExt(FileName, '.preflight.json'));
  finally
    Pdf.Free;
  end;
end;

De twee paden beantwoorden licht verschillende vragen. BuildPdfPreflightReport loopt via TPdf.ValidatePdfA, dat het open document naar een tijdelijke kopie opslaat, op die kopie dezelfde bytescan draait en daarna controles toevoegt die de PDFium-engine nodig hebben, zoals fonts die op een pagina worden gebruikt maar niet zijn ingesloten. ValidatePdfACompliance beoordeelt de exacte bytes op schijf. Voor implementatielimieten is het bytepad op het originele bestand het pad om te vertrouwen: hij rapporteert over het bestand dat u op het punt staat te archiveren, en hij draait in een buildstap of een afgesloten service waar een native DLL niet kan worden uitgerold. Gebruik het rapportpad wanneer u ook de controles op engelniveau wilt en de JSON-, SARIF- of JUnit-uitvoer voor een pijplijn

Waar verder gaan

Deze controles over een map heen draaien, beslissen welke uitkomsten een release blokkeren en het resultaat omzetten in een rapport dat mensen daadwerkelijk lezen, wordt behandeld in de doorloop van PDF/A-preflightvalidatie in Delphi en in de batch-preflightrapport-CLI. Welk PDF/A-deel en welk conformiteitsniveau u in eerste instantie moet nastreven, is het onderwerp van de notities over PDF/A-archiefcompliance; die keuze bepaalt onder meer of de integerlimiet geldt

De hier getoonde validators worden meegeleverd met PDFium Component voor Delphi, C++Builder en Lazarus, naast rendering, het invullen van formulieren en bewerking bovenop de PDFium-engine. De productpagina van PDFium Component lijst de ondersteunde conformiteitsstandaarden, edities en compilerversies op