Teknisk artikel

PDF/A-förhandsvalidering i Delphi med PDFium VCL

Ett arkivimportsteg avvisade ett parti "PDF/A-2b"-filer som öppnades utan problem i alla visare på skrivbordet. Leverantören svor att de var standardkonforma. Det var de inte: varje fil bar på en JavaScript-åtgärd gömd i katalogen, sådant som ett hastigt ögonkast aldrig fångar och som en fullständig PDF/A-validerare som veraPDF markerar direkt. Problemet var att ingen ville koppla in en Java-miljö i en Delphi-batchtjänst bara för att svara på en enda ja-eller-nej-fråga per fil. Det är det gap som ValidatePdfACompliance i PDFium Component fyller, och det är värt att förstå hur den når ett beslut utan att någonsin fullt ut tolka en innehållsström

Varför PDFium själv inte kan svara på detta

Det första att vara ärlig med: den medföljande pdfium.dll har ingen PDF/A-funktionalitet alls. Det finns ingen ConvertToPDFA, ingen OutputIntent-skrivare, inget XMP-API i den publika ytan. Alla delar av PDF/A i det här biblioteket, både skriv- och kontrollsidan, ligger i ren Pascal i FPdfPdfa.pas och arbetar med byte-nivåparsing plus inkrementella uppdateringar. Så när du anropar valideraren frågar du inte Chromiums renderare om någonting. Du kör en Pascal-token-scanner över filens strukturella byte

Det publika API:et är medvetet litet. En funktion läser en ström från position 0 och returnerar en post:

function ValidatePdfACompliance(Source: TStream): TPdfAValidationResult;

type
  TPdfAValidationResult = record
    Conformance: TPdfAConformance;        // pacUnknown, pacNone, pac1b, pac2u, ...
    Issues: TPdfAValidationIssues;        // a set of TPdfAValidationIssue
    function IsCompliant: Boolean;        // True only when level <> unknown/none
  end;                                    // AND Issues is empty

IsCompliant uttrycker regeln som spelar roll i ett kontrollsteg: en fil blir godkänd bara när en verklig överensstämmelsenivå har identifierats och problemuppsättningen är tom. En analys som lyckas men inte hittar någon pdfaid-markör landar på pacNone, vilket uttryckligen inte är ett godkänt resultat. Det här är samma poäng som batchrapport-CLI:t för förhandskontroll gör utifrån: en tom träfflista för en okänd fil är inte ett rent hälsotecken

Rensa strömmarnas innehåll före all token-skanning

Här är den enskilt viktigaste implementeringsdetaljen, och den som är lättast att göra fel om du skriver din egen scanner. Detektorn hittar överträdelser genom att söka efter avgränsade namntoken, sådant som /JavaScript, /LZWDecode, /BM. Om du skannar de råa filbyten kommer de inbäddade binära strömmarna, komprimerade bilder, ICC-profiler och fontprogram slumpmässigt att innehålla byteföljder som liknar de tokenen. Du kommer att rapportera /AA eller /3D som ”hittad” bara för att tre byte inuti en JPEG råkade stava det. Det är en fabrik för falska positiva

Lösningen är PdfStructureBytes: den går igenom filen och fyller byte mellan varje stream och endstream nyckelord med blanktecken, samtidigt som ordboksstrukturen lämnas intakt. Först därefter körs skanningen. Varje kontroll av namntoken i valideraren arbetar på denna rensade kopia. Om du bara tar med dig en sak från den här artikeln, ta med dig den. Samma disciplin speglas i PDF/UA-valideraren, som behåller sin egen kopia av rutinen eftersom de två standarderna utvecklas oberoende

De 29 problemen och vad vart och ett betyder

TPdfAValidationIssue är ett dokumenterat kontrakt. Ordinallägena är låsta eftersom DUnitX-tester, demos och rapportlagret alla är beroende av dem, så nya fynd läggs bara till sist. Från och med v1.63.0 finns det 29 medlemmar. De faller i några grupper:

  • Metadata och identitet: pvaiMissingXmpMetadata, pvaiMissingPdfAIdentifier, pvaiMissingTrailerId (ISO 19005-1 6.1.3), pvaiMissingXmpDates
  • Färg och utdata: pvaiMissingOutputIntent, pvaiMissingIccProfile, och pvaiMixedDeviceColorSpaces när både DeviceRGB och DeviceCMYK förekommer (6.2.3.3)
  • Hårda förbud för varje del: pvaiEncryptionPresent (en /Encrypt ordbok är helt förbjuden), pvaiJavaScriptPresent, pvaiForbiddenAction, pvaiAdditionalActions, pvaiLzwUsed, pvaiXfaPresent, pvaiNeedAppearancesTrue, pvaiForbiddenAnnotation
  • Teckensnitt: pvaiFontNotEmbedded och det strängare pvaiUnembeddedFont, plus pvaiUnicodeMappingMissing för ett Level U-påstående utan /ToUnicode
  • Taggning: pvaiLevelAStructureMissing när ett påstående om överensstämmelse=A saknar taggad struktur

De sex nyaste medlemmarna, tillagda på ordinalerna 24 till 29, täcker de knepiga fall som granskare faktiskt snubblar på: pvaiTrappedTrue (en /Trapped /True i Info-ordboken, en ”falsk vän” eftersom värdet måste vara False eller Unknown), pvaiForbiddenActionSubtype (Sound eller Movie används som åtgärd, inte bara som annotering), pvaiTransparentColorSpace (ett blandningsläge som inte är Normal eller en /CA//ca som inte är lika med 1.0), pvaiAnnotationDictViolation, pvaiUnembeddedFont, och pvaiMixedDeviceColorSpaces

Partsmedveten styrning: A-1 är strikt, A-2 och A-3 släpper på kraven

PDF/A är inte en enda regelbok. Tre saker som PDF/A-1 förbjuder är uttryckligen tillåtna från och med PDF/A-2: transparens (en /Transparency grupp eller en aktiv /SMask, 6.4), valfritt innehåll (/OCProperties, 6.1.13) och inbäddade filer (/EmbeddedFiles eller /EF, 6.1.11). En naiv validerare som markerar alla tre för varje fil kommer att förkasta fullt giltiga PDF/A-2-dokument i mängd

Så valideraren läser partsnumret från pdfaid-markören via PdfAPartOf och låser dessa kontroller bakom PartNo = 1. Blend-läges- och annoteringsalfa-kontrollerna för de nya transparensproblemen är också bara för part 1:

if PartNo = 1 then
begin
  if PdfHasName(Struct, '/BM') then
    if not PdfHasBMNormal(Struct) then          // only /Normal or /Compatible allowed
      Include(Result.Issues, pvaiTransparentColorSpace);
  if PdfHasCaNotOne(Struct, '/CA') or PdfHasCaNotOne(Struct, '/ca') then
    Include(Result.Issues, pvaiTransparentColorSpace);
end;

Ett konservativt standardval förtjänar att nämnas: när det inte finns någon pdfaid-markör alls behandlas delen som 1, den strängaste. Resonemanget är att en oidentifierad fil ska hållas till de stramaste reglerna i stället för att släppas igenom. JavaScript, förbjudna åtgärder, LZW, XFA, NeedAppearances, förbjudna annoteringar och icke-inbäddade teckensnitt förblir förbjudna för varje del, så de kontrollerna ligger aldrig bakom grinden

Expandera objektströmmar så att inget döljs

PDF 1.5 införde korsreferensströmmen och objektströmmen (/Type /ObjStm), och de skapar en blind fläck för en naiv byte-scanner. En katalog, en OutputIntent, en åtgärdsordbok, vad som helst som inte själv är en ström, kan Flate-komprimeras inne i en ObjStm. Skannar du den råa strukturen ser du inget av det och kan sedan rapportera en ren fil som allt annat än ren

PdfExpandObjectStreams stänger det gapet. Innan någon kontroll körs gör valideraren Data := PdfExpandObjectStreams(Data). Rutinen hittar varje ObjStm, läser dess /N och /First sidhuvud för att hämta de inneslutna objektnumren och offsetarna, inflaterar kroppen med PdfInflate (RTL:s zlib, System.ZLib i Delphi och zstream i FPC), och appenderar varje inneslutet objekt som en vanlig N 0 obj ... endobj till slutet av en kopia av bytena. De befintliga tokenkontrollerna hittar sedan de objekten utan någon ändring i logiken

Två begränsningar gör detta rent snarare än skört. Streamobjekt, Metadata, ICC-profil och fontprogram kan inte ligga i en objektström, bara icke-strömmade ordböcker kan det, så expansionen hanterar bara ordböcker och de tillagda objekten bär ingen stream nyckel att störa passet för rensning av innehållet. Och eftersom det tillagda innehållet hamnar efter %%EOF, hittar den bakåtgående sökningen från startxref fortfarande den ursprungliga trailer. Själva trailer för korsreferensströmmen hanterades redan tidigare, i v1.49.3, genom att läsa Root, Size och ID direkt ur den textuella xref-strömordboken, ett ämne som utforskas i den kompletterande artikeln om validering av objekt- och korsreferensströmmar; arbetet med objektströmmar behövde bara lägga till inflationssteget, utan behov av att avkoda xref-poster av typ 2 eller riva upp en PNG-prediktor

De ärliga gränserna för en byte-nivå-kontroll

Det här är ett förhandskontrollverktyg, inte en certifierad validerare, och gränserna är verkliga. Inbäddning av teckensnitt är en räkneheuristik, och att få den rätt krävde en korrigering som är värd att känna till. Den ursprungliga kontrollen använde PdfCountName('/FontDescriptor'), men varje teckensnitt bidrar med två /FontDescriptor token, en referens från teckensnittsordboken och en /Type i själva descriptor-objektet, så räknaren blev 2N mot N inbäddade program och testet blev alltid sant. Lösningen är PdfCountDescriptorRefs, som bara räknar den /FontDescriptor N G R referensformen, en per teckensnitt, och lägger till pvaiUnembeddedFont bara när de inbäddade programmen faktiskt är färre:

K := PdfCountDescriptorRefs(Struct);                 // one per font dict
Emb := PdfCountName(Struct, '/FontFile')
     + PdfCountName(Struct, '/FontFile2')
     + PdfCountName(Struct, '/FontFile3');
if (K > 0) and (Emb < K) then
  Include(Result.Issues, pvaiUnembeddedFont);

Även efter korrigeringen är den grov: ett blandat dokument där varje descriptor råkar ha någon FontFile kan fortfarande släppa igenom ett enskilt icke-konformt teckensnitt. Att expandera objektströmmar har också en känd sidoeffekt, det exponerar de standard-14-standardresurser som ett AcroForm /DR bär, som /Helv, och heuristiken rapporterar plikttroget dem som inte inbäddade även om veraPDF låter dem passera eftersom de aldrig faktiskt används för rendering. Kontroller på innehållsströmnivå, operator för operator (6.2.10), ligger helt utanför scope eftersom de skulle kräva fullständig innehållsparsering snarare än en byte-skanning. Se valideraren som en snabb, beroende-fri första grind som fångar överträdelser som markörinjektion inte kan fixa, och spara en full validerare för slutlig certifiering

Det här är kontrollhalvan av berättelsen. Den kompletterande skrivsidan, där SaveAsPdfA injicerar XMP, OutputIntent och sRGB ICC-profilen och ärligt nedgraderar en Level A-begäran som saknar taggad struktur, bygger på samma byte-nivåmaskineri. Båda halvorna levereras i PDFium Component for Delphi, ett enda VCL-paket ovanpå en ren Pascal-implementering av PDF/A utan någon extern runtime att installera